diff --git a/.github/scripts/build_assets/SeleniumRunner.py b/.github/scripts/build_assets/SeleniumRunner.py deleted file mode 100644 index b95d4a7c9..000000000 --- a/.github/scripts/build_assets/SeleniumRunner.py +++ /dev/null @@ -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() diff --git a/.github/scripts/build_assets/api_handler.py b/.github/scripts/build_assets/api_handler.py index a1f8c5dc9..f75a20ccd 100644 --- a/.github/scripts/build_assets/api_handler.py +++ b/.github/scripts/build_assets/api_handler.py @@ -2,10 +2,38 @@ 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 diff --git a/.github/scripts/build_assets/filehandler.py b/.github/scripts/build_assets/filehandler.py index 012706a2e..054431cd2 100644 --- a/.github/scripts/build_assets/filehandler.py +++ b/.github/scripts/build_assets/filehandler.py @@ -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) diff --git a/.github/scripts/build_assets/selenium_runner/BuildSeleniumRunner.py b/.github/scripts/build_assets/selenium_runner/BuildSeleniumRunner.py new file mode 100644 index 000000000..d69fa9cb6 --- /dev/null +++ b/.github/scripts/build_assets/selenium_runner/BuildSeleniumRunner.py @@ -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 diff --git a/.github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py b/.github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py new file mode 100644 index 000000000..50288a119 --- /dev/null +++ b/.github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py @@ -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...") diff --git a/.github/scripts/build_assets/selenium_runner/SeleniumRunner.py b/.github/scripts/build_assets/selenium_runner/SeleniumRunner.py new file mode 100644 index 000000000..7ada11631 --- /dev/null +++ b/.github/scripts/build_assets/selenium_runner/SeleniumRunner.py @@ -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() diff --git a/.github/scripts/build_assets/selenium_runner/enums.py b/.github/scripts/build_assets/selenium_runner/enums.py new file mode 100644 index 000000000..923a8a7cd --- /dev/null +++ b/.github/scripts/build_assets/selenium_runner/enums.py @@ -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 diff --git a/.github/scripts/build_assets/util.py b/.github/scripts/build_assets/util.py index 9d1a77011..9ecbc8075 100644 --- a/.github/scripts/build_assets/util.py +++ b/.github/scripts/build_assets/util.py @@ -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) diff --git a/.github/scripts/get_release_message.py b/.github/scripts/get_release_message.py deleted file mode 100644 index 28dfb88df..000000000 --- a/.github/scripts/get_release_message.py +++ /dev/null @@ -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() diff --git a/.github/scripts/icomoon_build.py b/.github/scripts/icomoon_build.py index c4484fb58..1a362c697 100644 --- a/.github/scripts/icomoon_build.py +++ b/.github/scripts/icomoon_build.py @@ -9,7 +9,7 @@ # 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() diff --git a/.github/scripts/icomoon_peek.py b/.github/scripts/icomoon_peek.py index ae81eb4b1..2dcc7143c 100644 --- a/.github/scripts/icomoon_peek.py +++ b/.github/scripts/icomoon_peek.py @@ -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): diff --git a/.github/workflows/build_icons.yml b/.github/workflows/build_icons.yml index d71c02dc6..161241c54 100644 --- a/.github/workflows/build_icons.yml +++ b/.github/workflows/build_icons.yml @@ -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} - ![Files Built]({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 diff --git a/.github/workflows/get_release_message.yml b/.github/workflows/get_release_message.yml deleted file mode 100644 index 1340610c8..000000000 --- a/.github/workflows/get_release_message.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/post_peek_screenshot.yml b/.github/workflows/post_peek_screenshot.yml index 23fb3858c..5370878a2 100644 --- a/.github/workflows/post_peek_screenshot.yml +++ b/.github/workflows/post_peek_screenshot.yml @@ -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,18 +80,31 @@ 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} - 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. + 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 take a look at it and decide whether to merge your PR.** + 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.) - Thank you for contributing to Devicon! I hope everything works out and your icons are accepted into the repo. + 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. Cheers, Peek Bot :blush: @@ -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 diff --git a/.gitignore b/.gitignore index 8d5f02294..13642b215 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ node_modules geckodriver.log __pycache__ *.pyc -new_icons.png \ No newline at end of file +new_icons.png +screenshots/ +release_message.txt diff --git a/devicon.css b/devicon-base.css old mode 100755 new mode 100644 similarity index 100% rename from devicon.css rename to devicon-base.css diff --git a/gulpfile.js b/gulpfile.js index aa494c6db..68faecf5e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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;