mirror of
https://github.com/konpa/devicon.git
synced 2025-04-21 05:11:56 +02:00
Feature: Peek and Build-Bot Upgrade (#806)
* Refactored peek script into a class * Post-peek workflow now upload the new screenshots * Refactored BuildSeleniumRunner into a class * Updated build_icons.yml to reflect new changes * Fixed issue with building icons that were already in the app * Build script will take screenshot of new icons * Update post peek yaml message * Added alerts * Peek script now check for strokes in icons * Updated post_peek's strokes in svgs message * Updated post_peek script's message * Updated post_peek's message * Refactored get_release_message into icomoon_build * Change devicon.css name to devicon-base.css * Updated post_peek message * Added update icon as a valid PR title for bot-peek * Add \n char to SVG after it gets optimized * Fixed error with 'update icon' regex * Build script now batch issues when upload SVG * Addressed build-bot's screenshot order * Apply suggestions from code review Co-authored-by: David Leal <halfpacho@gmail.com> Co-authored-by: David Leal <halfpacho@gmail.com>
This commit is contained in:
parent
e5aa8a9cad
commit
08aa325a84
267
.github/scripts/build_assets/SeleniumRunner.py
vendored
267
.github/scripts/build_assets/SeleniumRunner.py
vendored
@ -1,267 +0,0 @@
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from selenium.webdriver.firefox.webdriver import WebDriver
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
|
||||
|
||||
|
||||
class SeleniumRunner:
|
||||
"""
|
||||
A runner that upload and download Icomoon resources using Selenium.
|
||||
The WebDriver will use Firefox.
|
||||
"""
|
||||
|
||||
"""
|
||||
The long wait time for the driver in seconds.
|
||||
"""
|
||||
LONG_WAIT_IN_SEC = 25
|
||||
|
||||
"""
|
||||
The medium wait time for the driver in seconds.
|
||||
"""
|
||||
MED_WAIT_IN_SEC = 6
|
||||
|
||||
"""
|
||||
The short wait time for the driver in seconds.
|
||||
"""
|
||||
SHORT_WAIT_IN_SEC = 0.6
|
||||
|
||||
"""
|
||||
The Icomoon Url.
|
||||
"""
|
||||
ICOMOON_URL = "https://icomoon.io/app/#/select"
|
||||
|
||||
def __init__(self, download_path: str,
|
||||
geckodriver_path: str, headless: bool):
|
||||
"""
|
||||
Create a SeleniumRunner object.
|
||||
:param download_path: the location where you want to download
|
||||
the icomoon.zip to.
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
"""
|
||||
self.driver = None
|
||||
self.set_options(download_path, geckodriver_path, headless)
|
||||
|
||||
def set_options(self, download_path: str, geckodriver_path: str,
|
||||
headless: bool):
|
||||
"""
|
||||
Build the WebDriver with Firefox Options allowing downloads and
|
||||
set download to download_path.
|
||||
:param download_path: the location where you want to download
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
the icomoon.zip to.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
|
||||
:raises AssertionError: if the page title does not contain
|
||||
"IcoMoon App".
|
||||
"""
|
||||
options = Options()
|
||||
allowed_mime_types = "application/zip, application/gzip, application/octet-stream"
|
||||
# disable prompt to download from Firefox
|
||||
options.set_preference("browser.helperApps.neverAsk.saveToDisk", allowed_mime_types)
|
||||
options.set_preference("browser.helperApps.neverAsk.openFile", allowed_mime_types)
|
||||
|
||||
# set the default download path to downloadPath
|
||||
options.set_preference("browser.download.folderList", 2)
|
||||
options.set_preference("browser.download.dir", download_path)
|
||||
options.headless = headless
|
||||
|
||||
self.driver = WebDriver(options=options, executable_path=geckodriver_path)
|
||||
self.driver.get(self.ICOMOON_URL)
|
||||
assert "IcoMoon App" in self.driver.title
|
||||
# wait until the whole web page is loaded by testing the hamburger input
|
||||
WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "(//i[@class='icon-menu'])[2]"))
|
||||
)
|
||||
print("Accessed icomoon.io")
|
||||
|
||||
|
||||
def upload_icomoon(self, icomoon_json_path: str):
|
||||
"""
|
||||
Upload the icomoon.json to icomoon.io.
|
||||
:param icomoon_json_path: a path to the iconmoon.json.
|
||||
:raises TimeoutException: happens when elements are not found.
|
||||
"""
|
||||
print("Uploading icomoon.json file...")
|
||||
self.click_hamburger_input()
|
||||
|
||||
# find the file input and enter the file path
|
||||
import_btn = self.driver.find_element(By.XPATH, "(//li[@class='file'])[1]//input")
|
||||
import_btn.send_keys(icomoon_json_path)
|
||||
|
||||
try:
|
||||
confirm_btn = WebDriverWait(self.driver, SeleniumRunner.MED_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Yes']"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
except SeleniumTimeoutException as e:
|
||||
raise Exception("Cannot find the confirm button when uploading the icomoon.json" \
|
||||
"Ensure that the icomoon.json is in the correct format for Icomoon.io")
|
||||
|
||||
print("JSON file uploaded.")
|
||||
|
||||
def upload_svgs(self, svgs: List[str], screenshot_folder: str=""):
|
||||
"""
|
||||
Upload the SVGs provided in folder_info
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder. If
|
||||
the value is provided, it means the user want to take a screenshot
|
||||
of each icon.
|
||||
"""
|
||||
print("Uploading SVGs...")
|
||||
|
||||
edit_mode_btn = self.driver.find_element_by_css_selector(
|
||||
"div.btnBar button i.icon-edit"
|
||||
)
|
||||
edit_mode_btn.click()
|
||||
|
||||
self.click_hamburger_input()
|
||||
|
||||
for i in range(len(svgs)):
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
"li.file input[type=file]"
|
||||
)
|
||||
import_btn.send_keys(svgs[i])
|
||||
print(f"Uploaded {svgs[i]}")
|
||||
self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC, "Dismiss")
|
||||
self.click_on_just_added_icon(screenshot_folder, i)
|
||||
|
||||
# take a screenshot of the icons that were just added
|
||||
new_icons_path = str(Path(screenshot_folder, "new_icons.png").resolve())
|
||||
self.driver.save_screenshot(new_icons_path);
|
||||
|
||||
print("Finished uploading the svgs...")
|
||||
|
||||
def click_hamburger_input(self):
|
||||
"""
|
||||
Click the hamburger input until the pop up menu appears. This
|
||||
method is needed because sometimes, we need to click the hamburger
|
||||
input two times before the menu appears.
|
||||
:return: None.
|
||||
"""
|
||||
hamburger_input = self.driver.find_element_by_xpath(
|
||||
"(//i[@class='icon-menu'])[2]"
|
||||
)
|
||||
|
||||
menu_appear_callback = ec.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "h1 ul.menuList2")
|
||||
)
|
||||
|
||||
while not menu_appear_callback(self.driver):
|
||||
hamburger_input.click()
|
||||
|
||||
def test_for_possible_alert(self, wait_period: float, btn_text: str):
|
||||
"""
|
||||
Test for the possible alert when we upload the svgs.
|
||||
:param wait_period: the wait period for the possible alert
|
||||
in seconds.
|
||||
:param btn_text: the text that the alert's button will have.
|
||||
:return: None.
|
||||
"""
|
||||
try:
|
||||
dismiss_btn = WebDriverWait(self.driver, wait_period, 0.15).until(
|
||||
ec.element_to_be_clickable(
|
||||
(By.XPATH, f"//div[@class='overlay']//button[text()='{btn_text}']"))
|
||||
)
|
||||
dismiss_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass # nothing found => everything is good
|
||||
|
||||
def click_on_just_added_icon(self, screenshot_folder: str, index: int):
|
||||
"""
|
||||
Click on the most recently added icon so we can remove the colors
|
||||
and take a snapshot if needed.
|
||||
"""
|
||||
recently_uploaded_icon = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@id='set0']//mi-box[1]//div"))
|
||||
)
|
||||
recently_uploaded_icon.click()
|
||||
|
||||
self.remove_color_from_icon()
|
||||
|
||||
if screenshot_folder:
|
||||
screenshot_path = str(Path(screenshot_folder, f"screenshot_{index}.png").resolve())
|
||||
self.driver.save_screenshot(screenshot_path)
|
||||
print("Took screenshot and saved it at " + screenshot_path)
|
||||
|
||||
close_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-close")
|
||||
close_btn.click()
|
||||
|
||||
def remove_color_from_icon(self):
|
||||
"""
|
||||
Remove the color from the most recent uploaded icon.
|
||||
This is because some SVG have colors in them and we don't want to
|
||||
force contributors to remove them in case people want the colored SVGs.
|
||||
The color removal is also necessary so that the Icomoon-generated
|
||||
icons fit within one font symbol/ligiature.
|
||||
"""
|
||||
try:
|
||||
color_tab = WebDriverWait(self.driver, self.SHORT_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet"))
|
||||
)
|
||||
color_tab.click()
|
||||
|
||||
remove_color_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross")
|
||||
remove_color_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass # do nothing cause sometimes, the color tab doesn't appear in the site
|
||||
|
||||
def download_icomoon_fonts(self, zip_path: Path):
|
||||
"""
|
||||
Download the icomoon.zip from icomoon.io.
|
||||
:param zip_path: the path to the zip file after it's downloaded.
|
||||
"""
|
||||
# select all the svgs so that the newly added svg are part of the collection
|
||||
self.click_hamburger_input()
|
||||
select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//button[text()='Select All']"))
|
||||
)
|
||||
select_all_button.click()
|
||||
|
||||
print("Downloading Font files...")
|
||||
font_tab = self.driver.find_element_by_css_selector(
|
||||
"a[href='#/select/font']"
|
||||
)
|
||||
font_tab.click()
|
||||
|
||||
self.test_for_possible_alert(self.MED_WAIT_IN_SEC, "Continue")
|
||||
download_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span"))
|
||||
)
|
||||
download_btn.click()
|
||||
if self.wait_for_zip(zip_path):
|
||||
print("Font files downloaded.")
|
||||
else:
|
||||
raise TimeoutError(f"Couldn't find {zip_path} after download button was clicked.")
|
||||
|
||||
def wait_for_zip(self, zip_path: Path) -> bool:
|
||||
"""
|
||||
Wait for the zip file to be downloaded by checking for its existence
|
||||
in the download path. Wait time is self.LONG_WAIT_IN_SEC and check time
|
||||
is 1 sec.
|
||||
:param zip_path: the path to the zip file after it's
|
||||
downloaded.
|
||||
:return: True if the file is found within the allotted time, else
|
||||
False.
|
||||
"""
|
||||
end_time = time.time() + self.LONG_WAIT_IN_SEC
|
||||
while time.time() <= end_time:
|
||||
if zip_path.exists():
|
||||
return True
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the SeleniumRunner instance.
|
||||
"""
|
||||
print("Closing down SeleniumRunner...")
|
||||
self.driver.quit()
|
57
.github/scripts/build_assets/api_handler.py
vendored
57
.github/scripts/build_assets/api_handler.py
vendored
@ -2,10 +2,38 @@ import requests
|
||||
import sys
|
||||
import re
|
||||
|
||||
|
||||
def get_merged_pull_reqs_since_last_release(token):
|
||||
"""
|
||||
Get all the merged pull requests since the last release.
|
||||
"""
|
||||
stopPattern = r"^(r|R)elease v"
|
||||
pull_reqs = []
|
||||
found_last_release = False
|
||||
page = 1
|
||||
|
||||
print("Getting PRs since last release.")
|
||||
while not found_last_release:
|
||||
data = get_merged_pull_reqs(token, page)
|
||||
# assume we don't encounter it during the loop
|
||||
last_release_index = 101
|
||||
|
||||
for i in range(len(data)):
|
||||
if re.search(stopPattern, data[i]["title"]):
|
||||
found_last_release = True
|
||||
last_release_index = i
|
||||
break
|
||||
pull_reqs.extend(data[:last_release_index])
|
||||
page += 1
|
||||
|
||||
# should contain all the PRs since last release
|
||||
return pull_reqs
|
||||
|
||||
|
||||
def get_merged_pull_reqs(token, page):
|
||||
"""
|
||||
Get the merged pull requests based on page. There are
|
||||
100 results page. See https://docs.github.com/en/rest/reference/pulls
|
||||
100 results per page. See https://docs.github.com/en/rest/reference/pulls
|
||||
for more details on the parameters.
|
||||
:param token, a GitHub API token.
|
||||
:param page, the page number.
|
||||
@ -71,30 +99,3 @@ def find_all_authors(pull_req_data, token):
|
||||
authors.add(commit["commit"]["author"]["name"])
|
||||
print(f"This URL didn't have an `author` attribute: {pull_req_data['commits_url']}")
|
||||
return ", ".join(["@" + author for author in list(authors)])
|
||||
|
||||
|
||||
def get_merged_pull_reqs_since_last_release(token):
|
||||
"""
|
||||
Get all the merged pull requests since the last release.
|
||||
"""
|
||||
stopPattern = r"^(r|R)elease v"
|
||||
pull_reqs = []
|
||||
found_last_release = False
|
||||
page = 1
|
||||
|
||||
print("Getting PRs since last release.")
|
||||
while not found_last_release:
|
||||
data = get_merged_pull_reqs(token, page)
|
||||
# assume we don't encounter it during the loop
|
||||
last_release_index = 101
|
||||
|
||||
for i in range(len(data)):
|
||||
if re.search(stopPattern, data[i]["title"]):
|
||||
found_last_release = True
|
||||
last_release_index = i
|
||||
break
|
||||
pull_reqs.extend(data[:last_release_index])
|
||||
page += 1
|
||||
|
||||
# should contain all the PRs since last release
|
||||
return pull_reqs
|
||||
|
6
.github/scripts/build_assets/filehandler.py
vendored
6
.github/scripts/build_assets/filehandler.py
vendored
@ -91,7 +91,7 @@ def get_icon_svgs_paths(folder_path: Path, icon_info: dict,
|
||||
for font_version in icon_info["versions"]["font"]:
|
||||
# if it's an alias, we don't want to make it into an icon
|
||||
if is_alias(font_version, aliases):
|
||||
print(f"Skipping this font since it's an alias: {icon_info['name']}-{font_version}.svg")
|
||||
print(f"Finding SVG filepaths: skipping this font since it's an alias: {icon_info['name']}-{font_version}.svg")
|
||||
continue
|
||||
|
||||
file_name = f"{icon_info['name']}-{font_version}.svg"
|
||||
@ -177,7 +177,7 @@ def rename_extracted_files(extract_path: str):
|
||||
},
|
||||
{
|
||||
"old": Path(extract_path, "style.css"),
|
||||
"new": Path(extract_path, "devicon.css")
|
||||
"new": Path(extract_path, "devicon-base.css")
|
||||
}
|
||||
]
|
||||
|
||||
@ -203,7 +203,7 @@ def create_screenshot_folder(dir, screenshot_name: str="screenshots/"):
|
||||
try:
|
||||
os.mkdir(screenshot_folder)
|
||||
except FileExistsError:
|
||||
print(f"{screenshot_folder} already exist. Script will do nothing.")
|
||||
print(f"{screenshot_folder} already exist. Not creating new folder.")
|
||||
finally:
|
||||
return str(screenshot_folder)
|
||||
|
||||
|
171
.github/scripts/build_assets/selenium_runner/BuildSeleniumRunner.py
vendored
Normal file
171
.github/scripts/build_assets/selenium_runner/BuildSeleniumRunner.py
vendored
Normal file
@ -0,0 +1,171 @@
|
||||
from typing import List
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
|
||||
|
||||
from build_assets.selenium_runner.SeleniumRunner import SeleniumRunner
|
||||
from build_assets.selenium_runner.enums import IcomoonPage, IcomoonAlerts, IcomoonOptionState
|
||||
|
||||
class BuildSeleniumRunner(SeleniumRunner):
|
||||
def build_icons(self, icomoon_json_path: str,
|
||||
zip_path: Path, svgs: List[str], screenshot_folder: str):
|
||||
self.upload_icomoon(icomoon_json_path)
|
||||
# necessary so we can take screenshot of only the
|
||||
# recently uploaded icons later
|
||||
self.deselect_all_icons_in_top_set()
|
||||
self.upload_svgs(svgs, screenshot_folder)
|
||||
self.take_icon_screenshot(screenshot_folder)
|
||||
self.download_icomoon_fonts(zip_path)
|
||||
|
||||
def upload_icomoon(self, icomoon_json_path: str):
|
||||
"""
|
||||
Upload the icomoon.json to icomoon.io.
|
||||
:param icomoon_json_path: a path to the iconmoon.json.
|
||||
:raises TimeoutException: happens when elements are not found.
|
||||
"""
|
||||
print("Uploading icomoon.json file...")
|
||||
|
||||
# find the file input and enter the file path
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.GENERAL_IMPORT_BUTTON_CSS
|
||||
)
|
||||
import_btn.send_keys(icomoon_json_path)
|
||||
|
||||
try:
|
||||
confirm_btn = WebDriverWait(self.driver, SeleniumRunner.MED_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Yes']"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
except SeleniumTimeoutException as e:
|
||||
raise Exception("Cannot find the confirm button when uploading the icomoon.json" \
|
||||
"Ensure that the icomoon.json is in the correct format for Icomoon.io")
|
||||
|
||||
print("JSON file uploaded.")
|
||||
|
||||
def upload_svgs(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Upload the SVGs provided in svgs. This will upload the
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
"""
|
||||
print("Uploading SVGs...")
|
||||
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.SET_IMPORT_BUTTON_CSS
|
||||
)
|
||||
|
||||
# there could be at most 2 alerts when we upload an SVG.
|
||||
possible_alerts_amount = 2
|
||||
err_messages = []
|
||||
for i in range(len(svgs)):
|
||||
import_btn.send_keys(svgs[i])
|
||||
print(f"Uploading {svgs[i]}")
|
||||
|
||||
# see if there are stroke messages or replacing icon message
|
||||
# there should be none of the second kind
|
||||
for j in range(possible_alerts_amount):
|
||||
alert = self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.STROKES_GET_IGNORED_WARNING:
|
||||
message = f"SVG contained strokes: {svgs[i]}."
|
||||
err_messages.append(message)
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["DISMISS"])
|
||||
elif alert == IcomoonAlerts.REPLACE_OR_REIMPORT_ICON:
|
||||
message = f"Duplicated SVG: {svgs[i]}."
|
||||
err_messages.append(message)
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["REIMPORT"])
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
self.edit_svg()
|
||||
print(f"Finished editing icon.")
|
||||
|
||||
if err_messages != []:
|
||||
message = "BuildSeleniumRunner - Issues found when uploading SVGs:\n"
|
||||
raise Exception(message + '\n'.join(err_messages))
|
||||
|
||||
# take a screenshot of the svgs that were just added
|
||||
# select the latest icons
|
||||
self.switch_toolbar_option(IcomoonOptionState.SELECT)
|
||||
self.click_latest_icons_in_top_set(len(svgs))
|
||||
new_svgs_path = str(Path(screenshot_folder, "new_svgs.png").resolve())
|
||||
self.driver.save_screenshot(new_svgs_path);
|
||||
|
||||
print("Finished uploading the svgs...")
|
||||
|
||||
def take_icon_screenshot(self, screenshot_folder: str):
|
||||
"""
|
||||
Take the overview icon screenshot of the uploaded icons.
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
"""
|
||||
# take pictures
|
||||
print("Taking screenshot of the new icons...")
|
||||
self.go_to_generate_font_page()
|
||||
|
||||
# take an overall screenshot of the icons that were just added
|
||||
# also include the glyph count
|
||||
new_icons_path = str(Path(screenshot_folder, "new_icons.png").resolve())
|
||||
main_content_xpath = "/html/body/div[4]/div[2]/div/div[1]"
|
||||
main_content = self.driver.find_element_by_xpath(main_content_xpath)
|
||||
main_content.screenshot(new_icons_path)
|
||||
print("Saved screenshot of the new icons...")
|
||||
|
||||
def go_to_generate_font_page(self):
|
||||
"""
|
||||
Go to the generate font page. Also handles the "Deselect Icons
|
||||
with Strokes" alert.
|
||||
"""
|
||||
self.go_to_page(IcomoonPage.GENERATE_FONT)
|
||||
alert = self.test_for_possible_alert(self.MED_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.DESELECT_ICONS_CONTAINING_STROKES:
|
||||
message = f"One of SVGs contained strokes. This should not happen."
|
||||
raise Exception(message)
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
def download_icomoon_fonts(self, zip_path: Path):
|
||||
"""
|
||||
Download the icomoon.zip from icomoon.io. Also take a picture of
|
||||
what the icons look like.
|
||||
:param zip_path: the path to the zip file after it's downloaded.
|
||||
"""
|
||||
print("Downloading Font files...")
|
||||
if self.current_page != IcomoonPage.SELECTION:
|
||||
self.go_to_page(IcomoonPage.SELECTION)
|
||||
|
||||
self.select_all_icons_in_top_set()
|
||||
self.go_to_generate_font_page()
|
||||
|
||||
download_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span"))
|
||||
)
|
||||
download_btn.click()
|
||||
if self.wait_for_zip(zip_path):
|
||||
print("Font files downloaded.")
|
||||
else:
|
||||
raise TimeoutError(f"Couldn't find {zip_path} after download button was clicked.")
|
||||
|
||||
def wait_for_zip(self, zip_path: Path) -> bool:
|
||||
"""
|
||||
Wait for the zip file to be downloaded by checking for its existence
|
||||
in the download path. Wait time is self.LONG_WAIT_IN_SEC and check time
|
||||
is 1 sec.
|
||||
:param zip_path: the path to the zip file after it's
|
||||
downloaded.
|
||||
:return: True if the file is found within the allotted time, else
|
||||
False.
|
||||
"""
|
||||
end_time = time.time() + self.LONG_WAIT_IN_SEC
|
||||
while time.time() <= end_time:
|
||||
if zip_path.exists():
|
||||
return True
|
||||
time.sleep(1) # wait so we don't waste sys resources
|
||||
return False
|
100
.github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py
vendored
Normal file
100
.github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from build_assets.selenium_runner.SeleniumRunner import SeleniumRunner
|
||||
from build_assets.selenium_runner.enums import IcomoonPage, IcomoonAlerts
|
||||
|
||||
class PeekSeleniumRunner(SeleniumRunner):
|
||||
def peek(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Upload the SVGs and peek at how Icomoon interpret its SVGs and
|
||||
font versions.
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
:return an array of svgs with strokes as strings. These show which icon
|
||||
contains stroke.
|
||||
"""
|
||||
messages = self.peek_svgs(svgs, screenshot_folder)
|
||||
self.peek_icons(svgs, screenshot_folder)
|
||||
return messages
|
||||
|
||||
def peek_svgs(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Peek at the SVGs provided in svgs. This will look at how Icomoon
|
||||
interprets the SVGs as a font.
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
:return an array of svgs with strokes as strings. These show which icon
|
||||
contains stroke.
|
||||
"""
|
||||
print("Peeking SVGs...")
|
||||
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.GENERAL_IMPORT_BUTTON_CSS
|
||||
)
|
||||
|
||||
svgs_with_strokes = []
|
||||
for i in range(len(svgs)):
|
||||
import_btn.send_keys(svgs[i])
|
||||
print(f"Uploaded {svgs[i]}")
|
||||
|
||||
alert = self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.STROKES_GET_IGNORED_WARNING:
|
||||
print(f"- This icon contains strokes: {svgs[i]}")
|
||||
svg = Path(svgs[i])
|
||||
svgs_with_strokes.append(f"- {svg.name}")
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["DISMISS"])
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
self.edit_svg(screenshot_folder, i)
|
||||
|
||||
# take a screenshot of the svgs that were just added
|
||||
self.select_all_icons_in_top_set()
|
||||
new_svgs_path = str(Path(screenshot_folder, "new_svgs.png").resolve())
|
||||
icon_set_xpath = "/html/body/div[4]/div[1]/div[2]/div[1]"
|
||||
icon_set = self.driver.find_element_by_xpath(icon_set_xpath)
|
||||
icon_set.screenshot(new_svgs_path);
|
||||
|
||||
print("Finished peeking the svgs...")
|
||||
return svgs_with_strokes
|
||||
|
||||
def peek_icons(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Peek at the icon versions of the SVGs that were uploaded.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
"""
|
||||
print("Begin peeking at the icons...")
|
||||
# ensure all icons in the set is selected.
|
||||
self.select_all_icons_in_top_set()
|
||||
self.go_to_page(IcomoonPage.GENERATE_FONT)
|
||||
alert = self.test_for_possible_alert(self.MED_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.DESELECT_ICONS_CONTAINING_STROKES:
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["CONTINUE"])
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
# take an overall screenshot of the icons that were just added
|
||||
# also include the glyph count
|
||||
new_icons_path = str(Path(screenshot_folder, "new_icons.png").resolve())
|
||||
main_content_xpath = "/html/body/div[4]/div[2]/div/div[1]"
|
||||
main_content = self.driver.find_element_by_xpath(main_content_xpath)
|
||||
main_content.screenshot(new_icons_path);
|
||||
|
||||
# go downward so we get the oldest icon first
|
||||
len_ = len(svgs)
|
||||
for i in range(len_, 0, -1):
|
||||
xpath = f'//*[@id="glyphSet0"]/div[{i}]'
|
||||
icon_div = self.driver.find_element_by_xpath(xpath)
|
||||
|
||||
# crop the div out from the screenshot
|
||||
icon_screenshot = str(
|
||||
Path(screenshot_folder, f"new_icon_{len_ - i}.png").resolve()
|
||||
)
|
||||
icon_div.screenshot(icon_screenshot)
|
||||
|
||||
print("Finished peeking the icons...")
|
315
.github/scripts/build_assets/selenium_runner/SeleniumRunner.py
vendored
Normal file
315
.github/scripts/build_assets/selenium_runner/SeleniumRunner.py
vendored
Normal file
@ -0,0 +1,315 @@
|
||||
from pathlib import Path
|
||||
|
||||
from selenium.webdriver.firefox.webdriver import WebDriver
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
|
||||
|
||||
from build_assets.selenium_runner.enums import IcomoonOptionState, IcomoonPage, IcomoonAlerts
|
||||
|
||||
|
||||
class SeleniumRunner:
|
||||
"""
|
||||
A runner that upload and download Icomoon resources using Selenium.
|
||||
The WebDriver will use Firefox.
|
||||
"""
|
||||
|
||||
"""
|
||||
The long wait time for the driver in seconds.
|
||||
"""
|
||||
LONG_WAIT_IN_SEC = 25
|
||||
|
||||
"""
|
||||
The medium wait time for the driver in seconds.
|
||||
"""
|
||||
MED_WAIT_IN_SEC = 6
|
||||
|
||||
"""
|
||||
The short wait time for the driver in seconds.
|
||||
"""
|
||||
SHORT_WAIT_IN_SEC = 2.5
|
||||
|
||||
"""
|
||||
The short wait time for the driver in seconds.
|
||||
"""
|
||||
BRIEF_WAIT_IN_SEC = 0.6
|
||||
|
||||
"""
|
||||
The Icomoon Url.
|
||||
"""
|
||||
ICOMOON_URL = "https://icomoon.io/app/#/select"
|
||||
|
||||
"""
|
||||
General import button CSS for Icomoon site.
|
||||
"""
|
||||
GENERAL_IMPORT_BUTTON_CSS = "div#file input[type=file]"
|
||||
|
||||
"""
|
||||
Set import button CSS for Icomoon site.
|
||||
"""
|
||||
SET_IMPORT_BUTTON_CSS = "li.file input[type=file]"
|
||||
|
||||
"""
|
||||
The CSS of the tool bar options. There are more but
|
||||
these are the ones that we actually use.
|
||||
"""
|
||||
TOOLBAR_OPTIONS_CSS = {
|
||||
IcomoonOptionState.SELECT: "div.btnBar button i.icon-select",
|
||||
IcomoonOptionState.EDIT: "div.btnBar button i.icon-edit"
|
||||
}
|
||||
|
||||
"""
|
||||
The URL to go to different pages within the Icomoon domain.
|
||||
There are more but these are the ones that we actually use.
|
||||
"""
|
||||
PAGES_URL = {
|
||||
IcomoonPage.SELECTION: ICOMOON_URL,
|
||||
IcomoonPage.GENERATE_FONT: ICOMOON_URL + "/font"
|
||||
}
|
||||
|
||||
"""
|
||||
The different types of alerts that this workflow will encounter.
|
||||
It contains part of the text in the actual alert and buttons
|
||||
available to press. It's up to the user to know what button to
|
||||
press for which alert.
|
||||
"""
|
||||
ALERTS = {
|
||||
IcomoonAlerts.STROKES_GET_IGNORED_WARNING: {
|
||||
"text": "Strokes get ignored when generating fonts or CSH files.",
|
||||
"buttons": {
|
||||
"DISMISS": "Dismiss",
|
||||
}
|
||||
},
|
||||
IcomoonAlerts.REPLACE_OR_REIMPORT_ICON : {
|
||||
"text": "Replace existing icons?",
|
||||
"buttons": {
|
||||
"REPLACE": "Replace",
|
||||
"REIMPORT": "Reimport"
|
||||
}
|
||||
},
|
||||
IcomoonAlerts.DESELECT_ICONS_CONTAINING_STROKES: {
|
||||
"text": "Strokes get ignored when generating fonts.",
|
||||
"buttons": {
|
||||
"DESELECT": "Deselect",
|
||||
"CONTINUE": "Continue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, download_path: str,
|
||||
geckodriver_path: str, headless: bool):
|
||||
"""
|
||||
Create a SeleniumRunner object.
|
||||
:param download_path: the location where you want to download
|
||||
the icomoon.zip to.
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
"""
|
||||
self.driver = None
|
||||
# default values when we open Icomoon
|
||||
self.current_option_state = IcomoonOptionState.SELECT
|
||||
self.current_page = IcomoonPage.SELECTION
|
||||
self.set_browser_options(download_path, geckodriver_path, headless)
|
||||
|
||||
def set_browser_options(self, download_path: str, geckodriver_path: str,
|
||||
headless: bool):
|
||||
"""
|
||||
Build the WebDriver with Firefox Options allowing downloads and
|
||||
set download to download_path.
|
||||
:param download_path: the location where you want to download
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
the icomoon.zip to.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
|
||||
:raises AssertionError: if the page title does not contain
|
||||
"IcoMoon App".
|
||||
"""
|
||||
options = Options()
|
||||
allowed_mime_types = "application/zip, application/gzip, application/octet-stream"
|
||||
# disable prompt to download from Firefox
|
||||
options.set_preference("browser.helperApps.neverAsk.saveToDisk", allowed_mime_types)
|
||||
options.set_preference("browser.helperApps.neverAsk.openFile", allowed_mime_types)
|
||||
|
||||
# set the default download path to downloadPath
|
||||
options.set_preference("browser.download.folderList", 2)
|
||||
options.set_preference("browser.download.dir", download_path)
|
||||
options.headless = headless
|
||||
|
||||
print("Activating browser client...")
|
||||
self.driver = WebDriver(options=options, executable_path=geckodriver_path)
|
||||
self.driver.get(self.ICOMOON_URL)
|
||||
assert "IcoMoon App" in self.driver.title
|
||||
# wait until the whole web page is loaded by testing the hamburger input
|
||||
WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "(//i[@class='icon-menu'])[2]"))
|
||||
)
|
||||
print("Accessed icomoon.io")
|
||||
|
||||
def switch_toolbar_option(self, option: IcomoonOptionState):
|
||||
"""
|
||||
Switch the toolbar option to the option argument.
|
||||
:param option: an option from the toolbar of Icomoon.
|
||||
"""
|
||||
if self.current_option_state == option:
|
||||
return
|
||||
|
||||
option_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.TOOLBAR_OPTIONS_CSS[option]
|
||||
)
|
||||
option_btn.click()
|
||||
self.current_option_state = option
|
||||
|
||||
def click_hamburger_input(self):
|
||||
"""
|
||||
Click the hamburger input until the pop up menu appears. This
|
||||
method is needed because sometimes, we need to click the hamburger
|
||||
input two times before the menu appears.
|
||||
:return: None.
|
||||
"""
|
||||
top_set_hamburger_input_xpath = '//*[@id="setH2"]/button[1]/i'
|
||||
hamburger_input = self.driver.find_element_by_xpath(
|
||||
top_set_hamburger_input_xpath
|
||||
)
|
||||
|
||||
menu_appear_callback = ec.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "h1 ul.menuList2")
|
||||
)
|
||||
|
||||
while not menu_appear_callback(self.driver):
|
||||
hamburger_input.click()
|
||||
|
||||
def test_for_possible_alert(self, wait_period: float) -> IcomoonAlerts:
|
||||
"""
|
||||
Test for the possible alerts that might appear. Return the
|
||||
type of alert if one shows up.
|
||||
:param wait_period: the wait period for the possible alert
|
||||
in seconds.
|
||||
:return: an IcomoonAlerts enum representing the alert that was found.
|
||||
Else, return None.
|
||||
"""
|
||||
try:
|
||||
overlay_div = WebDriverWait(self.driver, wait_period, 0.15).until(
|
||||
ec.element_to_be_clickable(
|
||||
(By.XPATH, "//div[@class='overlay']"))
|
||||
)
|
||||
alert_message = overlay_div.text
|
||||
for alert in self.ALERTS.keys():
|
||||
if self.ALERTS[alert]["text"] in alert_message:
|
||||
return alert
|
||||
|
||||
return IcomoonAlerts.UNKNOWN
|
||||
except SeleniumTimeoutException:
|
||||
return None # nothing found => everything is good
|
||||
|
||||
def click_alert_button(self, btn_text: str):
|
||||
"""
|
||||
Click the button in the alert that matches the button text.
|
||||
:param btn_text: the text that the alert's button will have.
|
||||
"""
|
||||
try:
|
||||
button = WebDriverWait(self.driver, self.BRIEF_WAIT_IN_SEC, 0.15).until(
|
||||
ec.element_to_be_clickable(
|
||||
(By.XPATH, f"//div[@class='overlay']//button[text()='{btn_text}']"))
|
||||
)
|
||||
button.click()
|
||||
except SeleniumTimeoutException:
|
||||
return None # nothing found => everything is good
|
||||
|
||||
def edit_svg(self, screenshot_folder: str=None, index: int=None):
|
||||
"""
|
||||
Edit the SVG. This include removing the colors and take a
|
||||
snapshot if needed.
|
||||
:param screenshot_folder: a string or Path object. Point to
|
||||
where we store the screenshots. If truthy, take a screenshot
|
||||
and save it here.
|
||||
:param index: the index of the icon in its containing list.
|
||||
Used to differentiate the screenshots. Must be truthy if
|
||||
screenshot_folder is a truthy value.
|
||||
"""
|
||||
self.switch_toolbar_option(IcomoonOptionState.EDIT)
|
||||
self.click_latest_icons_in_top_set(1)
|
||||
|
||||
# strip the colors from the SVG.
|
||||
# This is because some SVG have colors in them and we don't want to
|
||||
# force contributors to remove them in case people want the colored SVGs.
|
||||
# The color removal is also necessary so that the Icomoon-generated
|
||||
# icons fit within one font symbol/ligiature.
|
||||
try:
|
||||
color_tab = WebDriverWait(self.driver, self.BRIEF_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet"))
|
||||
)
|
||||
color_tab.click()
|
||||
|
||||
remove_color_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross")
|
||||
remove_color_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass # do nothing cause sometimes, the color tab doesn't appear in the site
|
||||
|
||||
if screenshot_folder != None and index != None:
|
||||
edit_screen_selector = "div.overlay div.overlayWindow"
|
||||
screenshot_path = str(
|
||||
Path(screenshot_folder, f"new_svg_{index}.png").resolve()
|
||||
)
|
||||
edit_screen = self.driver.find_element_by_css_selector(
|
||||
edit_screen_selector)
|
||||
edit_screen.screenshot(screenshot_path)
|
||||
print("Took screenshot of svg and saved it at " + screenshot_path)
|
||||
|
||||
close_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-close")
|
||||
close_btn.click()
|
||||
|
||||
def click_latest_icons_in_top_set(self, amount: int):
|
||||
"""
|
||||
Click on the latest icons in the top set based on the amount passed in.
|
||||
This is state option agnostic (doesn't care if it's in SELECT or EDIT mode).
|
||||
:param amount: the amount of icons to click on from top left of the top
|
||||
set. Must be > 0.
|
||||
"""
|
||||
icon_base_xpath = '//div[@id="set0"]//mi-box[{}]//div'
|
||||
for i in range(1, amount + 1):
|
||||
icon_xpath = icon_base_xpath.format(i)
|
||||
latest_icon = self.driver.find_element_by_xpath(icon_xpath)
|
||||
latest_icon.click()
|
||||
|
||||
def select_all_icons_in_top_set(self):
|
||||
"""
|
||||
Select all the svgs in the top most (latest) set.
|
||||
"""
|
||||
self.click_hamburger_input()
|
||||
select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//button[text()='Select All']"))
|
||||
)
|
||||
select_all_button.click()
|
||||
|
||||
def deselect_all_icons_in_top_set(self):
|
||||
"""
|
||||
Select all the svgs in the top most (latest) set.
|
||||
"""
|
||||
self.click_hamburger_input()
|
||||
select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//button[text()='Deselect']"))
|
||||
)
|
||||
select_all_button.click()
|
||||
|
||||
def go_to_page(self, page: IcomoonPage):
|
||||
"""
|
||||
Go to the specified page in Icomoon. This used the URL rather than UI
|
||||
elements due to the inconsistent UI rendering.
|
||||
:param page: a valid page that can be accessed in Icomoon.
|
||||
"""
|
||||
if self.current_page == page:
|
||||
return
|
||||
|
||||
self.driver.get(self.PAGES_URL[page])
|
||||
self.current_page = page
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the SeleniumRunner instance.
|
||||
"""
|
||||
print("Closing down SeleniumRunner...")
|
||||
self.driver.quit()
|
28
.github/scripts/build_assets/selenium_runner/enums.py
vendored
Normal file
28
.github/scripts/build_assets/selenium_runner/enums.py
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class IcomoonOptionState(Enum):
|
||||
"""
|
||||
The state of the Icomoon toolbar options
|
||||
"""
|
||||
SELECT = 0,
|
||||
EDIT = 1
|
||||
|
||||
|
||||
class IcomoonPage(Enum):
|
||||
"""
|
||||
The available pages on the Icomoon website.
|
||||
"""
|
||||
SELECTION = 0,
|
||||
GENERATE_FONT = 1
|
||||
|
||||
|
||||
class IcomoonAlerts(Enum):
|
||||
"""
|
||||
The alerts that Icomoon displayed to the user. There
|
||||
could be more but these are the ones we usually see.
|
||||
"""
|
||||
STROKES_GET_IGNORED_WARNING = 0,
|
||||
REPLACE_OR_REIMPORT_ICON = 1,
|
||||
DESELECT_ICONS_CONTAINING_STROKES = 2,
|
||||
UNKNOWN = 3
|
14
.github/scripts/build_assets/util.py
vendored
14
.github/scripts/build_assets/util.py
vendored
@ -56,9 +56,13 @@ def find_object_added_in_pr(icons: List[dict], pr_title: str):
|
||||
:raise If no object can be found, raise an Exception.
|
||||
"""
|
||||
try:
|
||||
pattern = re.compile(r"(?<=^new icon: )\w+ (?=\(.+\))", re.I)
|
||||
icon_name = pattern.findall(pr_title)[0].lower().strip() # should only have one match
|
||||
icon = [icon for icon in icons if icon["name"] == icon_name][0]
|
||||
pattern = re.compile(r"(?<=^new icon: )\w+ (?=\(.+\))|(?<=^update icon: )\w+ (?=\(.+\))", re.I)
|
||||
icon_name_index = 0
|
||||
icon_name = pattern.findall(pr_title)[icon_name_index].lower().strip() # should only have one match
|
||||
icon = [icon for icon in icons if icon["name"] == icon_name][0]
|
||||
return icon
|
||||
except IndexError: # there are no match in the findall()
|
||||
raise Exception("Couldn't find an icon matching the name in the PR title.")
|
||||
except IndexError as e: # there are no match in the findall()
|
||||
print(e)
|
||||
message = "util.find_object_added_in_pr: Couldn't find an icon matching the name in the PR title.\n" \
|
||||
f"PR title is: '{pr_title}'"
|
||||
raise Exception(message)
|
||||
|
39
.github/scripts/get_release_message.py
vendored
39
.github/scripts/get_release_message.py
vendored
@ -1,39 +0,0 @@
|
||||
import requests
|
||||
from build_assets import arg_getters, api_handler, util
|
||||
import re
|
||||
|
||||
def main():
|
||||
try:
|
||||
print("Please wait a few seconds...")
|
||||
args = arg_getters.get_release_message_args()
|
||||
|
||||
# fetch first page by default
|
||||
data = api_handler.get_merged_pull_reqs_since_last_release(args.token)
|
||||
newIcons = []
|
||||
features = []
|
||||
|
||||
print("Parsing through the pull requests")
|
||||
for pullData in data:
|
||||
authors = api_handler.find_all_authors(pullData, args.token)
|
||||
markdown = f"- [{pullData['title']}]({pullData['html_url']}) by {authors}."
|
||||
|
||||
if api_handler.is_feature_icon(pullData):
|
||||
newIcons.append(markdown)
|
||||
else:
|
||||
features.append(markdown)
|
||||
|
||||
print("Constructing message")
|
||||
thankYou = "A huge thanks to all our maintainers and contributors for making this release possible!"
|
||||
iconTitle = f"**{len(newIcons)} New Icons**"
|
||||
featureTitle = f"**{len(features)} New Features**"
|
||||
finalString = "{0}\n\n {1}\n{2}\n\n {3}\n{4}".format(thankYou,
|
||||
iconTitle, "\n".join(newIcons), featureTitle, "\n".join(features))
|
||||
|
||||
print("--------------Here is the build message--------------\n", finalString)
|
||||
print("Script finished")
|
||||
except Exception as e:
|
||||
util.exit_with_err(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
61
.github/scripts/icomoon_build.py
vendored
61
.github/scripts/icomoon_build.py
vendored
@ -9,7 +9,7 @@ from typing import List, Dict
|
||||
|
||||
# pycharm complains that build_assets is an unresolved ref
|
||||
# don't worry about it, the script still runs
|
||||
from build_assets.SeleniumRunner import SeleniumRunner
|
||||
from build_assets.selenium_runner.BuildSeleniumRunner import BuildSeleniumRunner
|
||||
from build_assets import filehandler, arg_getters, util, api_handler
|
||||
|
||||
|
||||
@ -26,24 +26,29 @@ def main():
|
||||
|
||||
print(f"There are {len(new_icons)} icons to be build. Here are they:", *new_icons, sep = "\n")
|
||||
|
||||
print("Begin optimizing files")
|
||||
print("Begin optimizing files...")
|
||||
optimize_svgs(new_icons, args.icons_folder_path)
|
||||
|
||||
print("Updating the icomoon json")
|
||||
print("Updating the icomoon json...")
|
||||
update_icomoon_json(new_icons, args.icomoon_json_path)
|
||||
|
||||
print("Start the building icons process...")
|
||||
icon_svgs = filehandler.get_svgs_paths(
|
||||
new_icons, args.icons_folder_path, icon_versions_only=True)
|
||||
runner = SeleniumRunner(args.download_path,
|
||||
args.geckodriver_path, args.headless)
|
||||
runner.upload_icomoon(args.icomoon_json_path)
|
||||
runner.upload_svgs(icon_svgs)
|
||||
|
||||
zip_name = "devicon-v1.0.zip"
|
||||
zip_path = Path(args.download_path, zip_name)
|
||||
runner.download_icomoon_fonts(zip_path)
|
||||
screenshot_folder = filehandler.create_screenshot_folder("./")
|
||||
runner = BuildSeleniumRunner(args.download_path,
|
||||
args.geckodriver_path, args.headless)
|
||||
runner.build_icons(args.icomoon_json_path, zip_path,
|
||||
icon_svgs, screenshot_folder)
|
||||
|
||||
filehandler.extract_files(str(zip_path), args.download_path)
|
||||
filehandler.rename_extracted_files(args.download_path)
|
||||
|
||||
print("Creating the release message by querying the GitHub API...")
|
||||
get_release_message(args.token)
|
||||
|
||||
print("Task completed.")
|
||||
except TimeoutException as e:
|
||||
util.exit_with_err("Selenium Time Out Error: \n" + str(e))
|
||||
@ -60,6 +65,8 @@ def get_icons_for_building(icomoon_json_path: str, devicon_json_path: str, token
|
||||
:param icomoon_json_path - the path to the `icomoon.json`.
|
||||
:param devicon_json_path - the path to the `devicon.json`.
|
||||
:param token - the token to access the GitHub API.
|
||||
:return a list of dict containing info on the icons. These are
|
||||
from the `devicon.json`.
|
||||
"""
|
||||
devicon_json = filehandler.get_json_file_content(devicon_json_path)
|
||||
pull_reqs = api_handler.get_merged_pull_reqs_since_last_release(token)
|
||||
@ -72,6 +79,7 @@ def get_icons_for_building(icomoon_json_path: str, devicon_json_path: str, token
|
||||
new_icons.append(filtered_icon)
|
||||
|
||||
# get any icons that might not have been found by the API
|
||||
# sometimes happen due to the PR being opened before the latest build release
|
||||
new_icons_from_devicon_json = filehandler.find_new_icons_in_devicon_json(
|
||||
devicon_json_path, icomoon_json_path)
|
||||
|
||||
@ -136,5 +144,40 @@ def find_icomoon_icon_not_in_new_icons(icomoon_icon: Dict, new_icons: List, mess
|
||||
return True
|
||||
|
||||
|
||||
def get_release_message(token):
|
||||
"""
|
||||
Get the release message for the latest build and write
|
||||
the result in a file.
|
||||
:param token: the GitHub API token to access the API.
|
||||
"""
|
||||
# fetch first page by default
|
||||
data = api_handler.get_merged_pull_reqs_since_last_release(token)
|
||||
newIcons = []
|
||||
features = []
|
||||
|
||||
print("Parsing through the pull requests...")
|
||||
for pullData in data:
|
||||
authors = api_handler.find_all_authors(pullData, token)
|
||||
markdown = f"- [{pullData['title']}]({pullData['html_url']}) by {authors}."
|
||||
|
||||
if api_handler.is_feature_icon(pullData):
|
||||
newIcons.append(markdown)
|
||||
else:
|
||||
features.append(markdown)
|
||||
|
||||
print("Constructing message...")
|
||||
thankYou = "A huge thanks to all our maintainers and contributors for making this release possible!"
|
||||
iconTitle = f"**{len(newIcons)} New Icons**"
|
||||
featureTitle = f"**{len(features)} New Features**"
|
||||
finalString = "{0}\n\n {1}\n{2}\n\n {3}\n{4}".format(thankYou,
|
||||
iconTitle, "\n".join(newIcons),
|
||||
featureTitle, "\n".join(features))
|
||||
|
||||
print("--------------Here is the build message--------------\n", finalString)
|
||||
release_message_path = "./release_message.txt"
|
||||
filehandler.write_to_file(release_message_path, finalString)
|
||||
print("Script finished")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
23
.github/scripts/icomoon_peek.py
vendored
23
.github/scripts/icomoon_peek.py
vendored
@ -1,11 +1,4 @@
|
||||
from typing import List
|
||||
import re
|
||||
import sys
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
|
||||
# pycharm complains that build_assets is an unresolved ref
|
||||
# don't worry about it, the script still runs
|
||||
from build_assets.SeleniumRunner import SeleniumRunner
|
||||
from build_assets.selenium_runner.PeekSeleniumRunner import PeekSeleniumRunner
|
||||
from build_assets import filehandler, arg_getters
|
||||
from build_assets import util
|
||||
|
||||
@ -21,19 +14,23 @@ def main():
|
||||
check_devicon_object(filtered_icon)
|
||||
print("Icon being checked:", filtered_icon, sep = "\n", end='\n\n')
|
||||
|
||||
runner = SeleniumRunner(args.download_path, args.geckodriver_path, args.headless)
|
||||
runner = PeekSeleniumRunner(args.download_path, args.geckodriver_path, args.headless)
|
||||
svgs = filehandler.get_svgs_paths([filtered_icon], args.icons_folder_path, True)
|
||||
screenshot_folder = filehandler.create_screenshot_folder("./")
|
||||
runner.upload_svgs(svgs, screenshot_folder)
|
||||
svgs_with_strokes = runner.peek(svgs, screenshot_folder)
|
||||
print("Task completed.")
|
||||
|
||||
# no errors, do this so upload-artifact won't fail
|
||||
filehandler.write_to_file("./err_messages.txt", "0")
|
||||
message = ""
|
||||
if svgs_with_strokes != []:
|
||||
svgs_str = "\n\n".join(svgs_with_strokes)
|
||||
message = "\n### WARNING -- Strokes detected in the following SVGs:\n" + svgs_str + "\n"
|
||||
filehandler.write_to_file("./err_messages.txt", message)
|
||||
except Exception as e:
|
||||
filehandler.write_to_file("./err_messages.txt", str(e))
|
||||
util.exit_with_err(e)
|
||||
finally:
|
||||
runner.close()
|
||||
if runner is not None:
|
||||
runner.close()
|
||||
|
||||
|
||||
def check_devicon_object(icon: dict):
|
||||
|
36
.github/workflows/build_icons.yml
vendored
36
.github/workflows/build_icons.yml
vendored
@ -42,27 +42,45 @@ jobs:
|
||||
uses: devicons/public-upload-to-imgur@v2.2.2
|
||||
if: success()
|
||||
with:
|
||||
path: ./new_icons.png
|
||||
# will have "new_icons.png" and "new_svgs.png"
|
||||
# in that order (cause sorted alphabetically)
|
||||
path: ./screenshots/*.png
|
||||
client_id: ${{secrets.IMGUR_CLIENT_ID}}
|
||||
|
||||
- name: Get the release message from file
|
||||
id: release_message_step
|
||||
uses: juliangruber/read-file-action@v1.0.0
|
||||
with:
|
||||
# taken from icomoon_build.py's get_release_message()
|
||||
path: ./release_message.txt
|
||||
|
||||
- name: Create Pull Request
|
||||
if: success()
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
env:
|
||||
MESSAGE: |
|
||||
What's up!
|
||||
Hello,
|
||||
|
||||
I'm Devicon's Build Bot and I just built some new font files and devicon.min.css file.
|
||||
|
||||
Here are all the files that were built into icons (the new ones are those without highlight):
|
||||
Here are all the **SVGs** that were uploaded (the new ones are those without highlight):
|
||||
|
||||

|
||||
{0}
|
||||
|
||||
Here is what they look like as icons:
|
||||
|
||||
{1}
|
||||
|
||||
The devicon.min.css file contains:
|
||||
-The icon content
|
||||
-The aliases
|
||||
-The colored classes
|
||||
|
||||
I also compiled a list of new features and icons that were added since last release.
|
||||
```
|
||||
{2}
|
||||
```
|
||||
|
||||
More information can be found in the GitHub Action logs for this workflow.
|
||||
|
||||
Adios,
|
||||
@ -71,5 +89,13 @@ jobs:
|
||||
branch: 'bot/build-result'
|
||||
commit-message: 'Built new icons, icomoon.json and devicon.css'
|
||||
title: 'bot:build new icons, icomoon.json and devicon.css'
|
||||
body: ${{ format(env.MESSAGE, fromJSON(steps.imgur_step.outputs.imgur_urls)[0] ) }}
|
||||
body: >
|
||||
${{
|
||||
format(
|
||||
env.MESSAGE,
|
||||
fromJSON(steps.imgur_step.outputs.markdown_urls)[1],
|
||||
fromJSON(steps.imgur_step.outputs.markdown_urls)[0],
|
||||
steps.release_message_step.outputs.content
|
||||
)
|
||||
}}
|
||||
delete-branch: true
|
||||
|
23
.github/workflows/get_release_message.yml
vendored
23
.github/workflows/get_release_message.yml
vendored
@ -1,23 +0,0 @@
|
||||
name: Get Release Message
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
name: Get features since last release
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python v3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r ./.github/scripts/requirements.txt
|
||||
|
||||
- name: Run the get_release_message.py
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: python ./.github/scripts/get_release_message.py $GITHUB_TOKEN
|
79
.github/workflows/post_peek_screenshot.yml
vendored
79
.github/workflows/post_peek_screenshot.yml
vendored
@ -39,6 +39,22 @@ jobs:
|
||||
with:
|
||||
path: ./err_messages/err_messages.txt
|
||||
|
||||
# using this trigger allowed us to access secrets
|
||||
- name: Upload screenshot of the SVGs gotten from the artifacts
|
||||
id: svgs_overview_img_step
|
||||
if: env.PEEK_STATUS == 'success' && success()
|
||||
uses: devicons/public-upload-to-imgur@v2.2.2
|
||||
with:
|
||||
path: ./screenshots/new_svgs.png
|
||||
client_id: ${{secrets.IMGUR_CLIENT_ID}}
|
||||
|
||||
- name: Upload zoomed in screenshot of the SVGs gotten from the artifacts
|
||||
id: svgs_detailed_img_step
|
||||
uses: devicons/public-upload-to-imgur@v2.2.2
|
||||
if: env.PEEK_STATUS == 'success' && success()
|
||||
with:
|
||||
path: ./screenshots/new_svg_*.png
|
||||
client_id: ${{secrets.IMGUR_CLIENT_ID}}
|
||||
|
||||
- name: Upload screenshot of the newly made icons gotten from the artifacts
|
||||
id: icons_overview_img_step
|
||||
@ -53,7 +69,7 @@ jobs:
|
||||
uses: devicons/public-upload-to-imgur@v2.2.2
|
||||
if: env.PEEK_STATUS == 'success' && success()
|
||||
with:
|
||||
path: ./screenshots/screenshot_*.png
|
||||
path: ./screenshots/new_icon_*.png
|
||||
client_id: ${{secrets.IMGUR_CLIENT_ID}}
|
||||
|
||||
- name: Comment on the PR about the result - Success
|
||||
@ -64,19 +80,32 @@ jobs:
|
||||
Hi there,
|
||||
|
||||
I'm Devicons' Peek Bot and I just peeked at the icons that you wanted to add using [icomoon.io](https://icomoon.io/app/#/select).
|
||||
Here is the result below (top left):
|
||||
|
||||
{0}
|
||||
|
||||
Here are the zoomed-in screenshots of the added icons:
|
||||
Here are the SVGs as intepreted by Icomoon when we upload the files:
|
||||
{1}
|
||||
|
||||
Here are the zoomed-in screenshots of the added icons as **SVGs**. This is how Icomoon intepret the uploaded SVGs:
|
||||
{2}
|
||||
|
||||
Here are the icons that will be generated by Icomoon:
|
||||
{3}
|
||||
|
||||
Here are the zoomed-in screenshots of the added icons as **icons**. This is what the font will look like:
|
||||
{4}
|
||||
|
||||
You can click on the pictures and zoom on them if needed.
|
||||
|
||||
The maintainers will now check for:
|
||||
1. The number of Glyphs matches the number of SVGs that were selected.
|
||||
2. The icons (second group of pictures) look the same as the SVGs (first group of pictures).
|
||||
3. The icons are of high quality (legible, matches the official logo, etc.)
|
||||
|
||||
In case of font issues, it might be caused by Icomoon not accepting strokes in the SVGs. Check this [doc](https://icomoon.io/#faq/importing) for more details and fix the issues as instructed by Icomoon and update this PR once you are done.
|
||||
|
||||
Thank you for contributing to Devicon! I hope that your icons are accepted into the repository.
|
||||
|
||||
Note: If the images don't show up, it's probably because it has been autodeleted by Imgur after 6 months due to our API choice.
|
||||
|
||||
**The maintainers will now take a look at it and decide whether to merge your PR.**
|
||||
|
||||
Thank you for contributing to Devicon! I hope everything works out and your icons are accepted into the repo.
|
||||
|
||||
Cheers,
|
||||
Peek Bot :blush:
|
||||
with:
|
||||
@ -84,12 +113,19 @@ jobs:
|
||||
issue_number: ${{ steps.pr_num_reader.outputs.content }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body: >
|
||||
${{ format(env.MESSAGE,
|
||||
fromJSON(steps.icons_overview_img_step.outputs.markdown_urls)[0],
|
||||
join(fromJSON(steps.icons_detailed_img_step.outputs.markdown_urls), '')) }}
|
||||
${{
|
||||
format(
|
||||
env.MESSAGE,
|
||||
steps.err_message_reader.outputs.content,
|
||||
fromJSON(steps.svgs_overview_img_step.outputs.markdown_urls)[0],
|
||||
join(fromJSON(steps.svgs_detailed_img_step.outputs.markdown_urls), ' '),
|
||||
fromJSON(steps.icons_overview_img_step.outputs.markdown_urls)[0],
|
||||
join(fromJSON(steps.icons_detailed_img_step.outputs.markdown_urls), ' ')
|
||||
)
|
||||
}}
|
||||
|
||||
- name: Comment on the PR about the result - Failure
|
||||
if: failure() || env.PEEK_STATUS == 'failure'
|
||||
if: env.PEEK_STATUS == 'failure'
|
||||
uses: jungwinter/comment@v1 # let us comment on a specific PR
|
||||
env:
|
||||
MESSAGE: |
|
||||
@ -116,3 +152,20 @@ jobs:
|
||||
issue_number: ${{ steps.pr_num_reader.outputs.content }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body: ${{ format(env.MESSAGE, steps.err_message_reader.outputs.content) }}
|
||||
- name: Comment on the PR about the result - Failure
|
||||
if: failure()
|
||||
uses: jungwinter/comment@v1 # let us comment on a specific PR
|
||||
env:
|
||||
MESSAGE: |
|
||||
Hi there,
|
||||
|
||||
I'm Devicons' Peek Bot and we've ran into a problem with the `post_peek_screenshot` workflow.
|
||||
The maintainers will take a look and fix the issue. Please wait for further instructions.
|
||||
|
||||
Thank you,
|
||||
Peek Bot :relaxed:
|
||||
with:
|
||||
type: create
|
||||
issue_number: ${{ steps.pr_num_reader.outputs.content }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body: env.MESSAGE
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -4,4 +4,6 @@ node_modules
|
||||
geckodriver.log
|
||||
__pycache__
|
||||
*.pyc
|
||||
new_icons.png
|
||||
new_icons.png
|
||||
screenshots/
|
||||
release_message.txt
|
||||
|
0
devicon.css → devicon-base.css
Executable file → Normal file
0
devicon.css → devicon-base.css
Executable file → Normal file
@ -1,11 +1,13 @@
|
||||
const gulp = require("gulp");
|
||||
const svgmin = require("gulp-svgmin");
|
||||
const sass = require("gulp-sass")(require("sass"));
|
||||
const footer = require("gulp-footer");
|
||||
const yargs = require("yargs");
|
||||
const fsPromise = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
// global const
|
||||
const deviconBaseCSSName = "devicon-base.css"
|
||||
const deviconJSONName = "devicon.json";
|
||||
const aliasSCSSName = "devicon-alias.scss";
|
||||
const colorsCSSName = "devicon-colors.css";
|
||||
@ -21,7 +23,7 @@ async function createDeviconMinCSS() {
|
||||
await createCSSFiles();
|
||||
|
||||
let deviconMinPath = path.join(__dirname, finalMinSCSSName);
|
||||
// recall that devicon-alias.scss imported the devicon.css => don't need
|
||||
// recall that devicon-alias.scss imported the devicon-base.css => don't need
|
||||
// to reimport that file.
|
||||
const fileContent = `@use "${aliasSCSSName}";@use "${colorsCSSName}";`;
|
||||
await fsPromise.writeFile(deviconMinPath, fileContent, "utf8");
|
||||
@ -59,7 +61,7 @@ async function createCSSFiles() {
|
||||
*/
|
||||
function createAliasSCSS(deviconJson) {
|
||||
let statements = deviconJson.map(createAliasStatement).join(" ");
|
||||
let sass = `@use "devicon";${statements}`;
|
||||
let sass = `@use "${deviconBaseCSSName}";${statements}`;
|
||||
let sassPath = path.join(__dirname, aliasSCSSName);
|
||||
return fsPromise.writeFile(sassPath, sass, "utf8");
|
||||
}
|
||||
@ -156,6 +158,7 @@ function optimizeSvg() {
|
||||
return gulp
|
||||
.src(svgGlob)
|
||||
.pipe(svgmin(configOptionCallback))
|
||||
.pipe(footer("\n"))
|
||||
.pipe(
|
||||
gulp.dest(file => {
|
||||
return file.base;
|
||||
|
Loading…
x
Reference in New Issue
Block a user