diff --git a/.github/scripts/build_assets/arg_getters.py b/.github/scripts/build_assets/arg_getters.py index 8f82f0e6d..81eb7abbf 100644 --- a/.github/scripts/build_assets/arg_getters.py +++ b/.github/scripts/build_assets/arg_getters.py @@ -2,9 +2,9 @@ from build_assets.PathResolverAction import PathResolverAction -def get_selenium_runner_args(has_token=True, peek_mode=False): +def get_selenium_runner_args(has_token=True): """ - Get the commandline arguments for the icomoon_peek.py and + Get the commandline arguments for the icomoon_peek.py and icomoon_build.py. """ parser = ArgumentParser(description="Upload svgs to Icomoon to create icon files.") @@ -33,13 +33,14 @@ def get_selenium_runner_args(has_token=True, peek_mode=False): help="The download destination of the Icomoon files", action=PathResolverAction) - if peek_mode: - parser.add_argument("pr_title", - help="The title of the PR that we are peeking at") if has_token != False: parser.add_argument("token", help="The GitHub token to access the GitHub REST API.") + parser.add_argument("changed_files", + help="List of SVG files changed since the last release/tag", + nargs="+") + return parser.parse_args() @@ -49,9 +50,6 @@ def get_check_icon_pr_args(): """ parser = ArgumentParser(description="Check the SVGs to ensure their attributes are correct. Run whenever a PR is opened") - parser.add_argument("pr_title", - help="The title of the PR that we are peeking at") - parser.add_argument("icons_folder_path", help="The path to the icons folder", action=PathResolverAction) @@ -60,6 +58,10 @@ def get_check_icon_pr_args(): help="The path to the devicon.json", action=PathResolverAction) + parser.add_argument("changed_files", + help="List of SVG files changed in the PR", + nargs="+") + return parser.parse_args() diff --git a/.github/scripts/build_assets/util.py b/.github/scripts/build_assets/util.py index e98b2a900..43461cd28 100644 --- a/.github/scripts/build_assets/util.py +++ b/.github/scripts/build_assets/util.py @@ -50,29 +50,26 @@ def set_env_var(key: str, value: str, delimiter: str='~'): raise Exception("This function doesn't support this platform: " + platform.system()) -def find_object_added_in_pr(icons: List[dict], pr_title: str): +def find_changed_icons(icons: List[dict], changed_files: List[str]) -> List[dict]: """ - Find the icon name from the PR title. + Find the changed icons provided in the changed_files list. :param icons, a list of the font objects found in the devicon.json. - :pr_title, the title of the PR that this workflow was called on. - :return a dictionary with the "name" - entry's value matching the name in the pr_title. - :raise If no object can be found, raise an Exception. + :param changed_files, SVG files changed in the PR or since the last release/tag. + :return a list of dictionaries with the "name" + entry values matching the name of changed icons. """ - try: - 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 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) + filtered_icons = [] + icon_names = [] + for file in changed_files: + icon_name = Path(file).parent.name + icon = [icon for icon in icons if icon["name"] == icon_name] + if len(icon) > 0 and icon_name not in icon_names: + icon_names.append(icon_name) + filtered_icons.extend(icon) + return filtered_icons -def is_svg_in_font_attribute(svg_file_path: Path, devicon_object: dict): +def is_svg_in_font_attribute(svg_file_path: Path, devicon_object: dict): """ Check if svg is in devicon.json's font attribute. :param svg_file_path, the path to a single svg icon diff --git a/.github/scripts/check_icon_pr.py b/.github/scripts/check_icon_pr.py index dc5eec771..01d73e801 100644 --- a/.github/scripts/check_icon_pr.py +++ b/.github/scripts/check_icon_pr.py @@ -18,42 +18,45 @@ def main(): try: all_icons = filehandler.get_json_file_content(args.devicon_json_path) - devicon_err_msg = [] - #First check if devicon.json is sorted + err_msg = [] + # First check if devicon.json is sorted if sorted(all_icons, key=lambda d: d['name']) != all_icons: - devicon_err_msg.append(f"devicon.json is not sorted correctly.\nPlease make sure that your icon is added in the `devicon.json` file at the correct alphabetic position\nas seen here: https://github.com/devicons/devicon/wiki/Updating-%60devicon.json%60") + err_msg.append("devicon.json is not sorted correctly.\nPlease make sure that your icon is added in the `devicon.json` file at the correct alphabetic position\nas seen here: https://github.com/devicons/devicon/wiki/Updating-%60devicon.json%60") # get only the icon object that has the name matching the pr title - filtered_icon = util.find_object_added_in_pr(all_icons, args.pr_title) - print("Checking devicon.json object: " + str(filtered_icon)) - devicon_err_msg.append(check_devicon_object(filtered_icon)) - - # check the file names - filename_err_msg = "" - svgs = None - try: - svgs = filehandler.get_svgs_paths([filtered_icon], args.icons_folder_path, as_str=False) - print("SVGs to check: ", *svgs, sep='\n') - except ValueError as e: - filename_err_msg = "Error found regarding filenames:\n- " + e.args[0] - - # check the svgs - if svgs is None or len(svgs) == 0: - print("No SVGs to check, ending script.") - svg_err_msg = "Error checking SVGs: no SVGs to check. Might be caused by above issues." - else: - svg_err_msg = check_svgs(svgs, filtered_icon) - - err_msg = [] - if devicon_err_msg != []: - err_msg.extend(devicon_err_msg) - - if filename_err_msg != "": - err_msg.append(filename_err_msg) - - if svg_err_msg != "": - err_msg.append(svg_err_msg) - + filtered_icons = util.find_changed_icons(all_icons, args.changed_files) + for filtered_icon in filtered_icons: + devicon_err_msg = [] + print("Checking devicon.json object: " + str(filtered_icon)) + devicon_err_msg.append(check_devicon_object(filtered_icon)) + + # check the file names + filename_err_msg = "" + svgs = None + try: + svgs = filehandler.get_svgs_paths([filtered_icon], args.icons_folder_path, as_str=False) + print("SVGs to check: ", *svgs, sep='\n') + except ValueError as e: + filename_err_msg = "Error found regarding filenames:\n- " + e.args[0] + + # check the svgs + if svgs is None or len(svgs) == 0: + print("No SVGs to check for this icon.") + svg_err_msg = "Error checking SVGs: no SVGs to check. Might be caused by above issues." + else: + svg_err_msg = check_svgs(svgs, filtered_icon) + + if devicon_err_msg: + err_msg.extend(devicon_err_msg) + + if filename_err_msg: + err_msg.append(filename_err_msg) + + if svg_err_msg: + err_msg.append(svg_err_msg) + + err_msg = list(filter(None, err_msg)) # remove empty strings from err_msg + print("Error messages: ", err_msg) filehandler.write_to_file("./err_messages.txt", "\n\n".join(err_msg)) print("Task completed.") except Exception as e: @@ -109,7 +112,7 @@ def check_devicon_object(icon: dict): err_msgs.append(f"- Invalid version name in versions['svg']: '{version}'. Must match regexp: (original|plain|line)(-wordmark)?") except KeyError: err_msgs.append("- missing key: 'svg' in 'versions'.") - + try: if type(icon["versions"]["font"]) != list or len(icon["versions"]["svg"]) == 0: err_msgs.append("- must contain at least 1 font version in a list.") @@ -160,7 +163,7 @@ def check_devicon_object(icon: dict): if len(err_msgs) > 0: message = "Error found in \"devicon.json\" for \"{}\" entry: \n{}".format(icon["name"], "\n".join(err_msgs)) return message - return "" + return "" def check_svgs(svg_file_paths: List[Path], devicon_object: dict): diff --git a/.github/scripts/icomoon_build.py b/.github/scripts/icomoon_build.py index 3dafe3a68..7789916d9 100644 --- a/.github/scripts/icomoon_build.py +++ b/.github/scripts/icomoon_build.py @@ -22,11 +22,11 @@ def main(): logfile = open("log.txt", "w") try: args = arg_getters.get_selenium_runner_args() - new_icons = get_icons_for_building(args.icomoon_json_path, args.devicon_json_path, args.token, logfile) + new_icons = get_icons_for_building(args.devicon_json_path, args.changed_files) if len(new_icons) == 0: sys.exit("No files need to be uploaded. Ending script...") - print(f"There are {len(new_icons)} icons to be build. Here are they:", *new_icons, sep = "\n", file=logfile) + print(f"There are {len(new_icons)} icons to be build. Here are they:", *new_icons, sep="\n", file=logfile) print("Begin optimizing files...", file=logfile) optimize_svgs(new_icons, args.icons_folder_path, logfile=logfile) @@ -39,7 +39,7 @@ def main(): new_icons, args.icons_folder_path, icon_versions_only=True) zip_name = "devicon-v1.0.zip" zip_path = Path(args.download_path, zip_name) - screenshot_folder = filehandler.create_screenshot_folder("./") + screenshot_folder = filehandler.create_screenshot_folder("./") runner = BuildSeleniumRunner(args.download_path, args.geckodriver_path, args.headless, log_output=logfile) @@ -65,43 +65,24 @@ def main(): finally: print("Exiting", file=logfile) if runner is not None: - runner.close() + runner.close() logfile.close() -def get_icons_for_building(icomoon_json_path: str, devicon_json_path: str, token: str, logfile: FileIO): +def get_icons_for_building(devicon_json_path: str, changed_files: List[str]) -> List[dict]: """ Get the icons for building. - :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. - :param logfile. - :return a list of dict containing info on the icons. These are + :param changed_files - the list of changed files since the last release/tag. + + :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, logfile) - new_icons = [] - - for pull_req in pull_reqs: - if api_handler.is_feature_icon(pull_req): - filtered_icon = util.find_object_added_in_pr(devicon_json, pull_req["title"]) - if filtered_icon not in new_icons: - 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) + return util.find_changed_icons(devicon_json, changed_files) - for icon in new_icons_from_devicon_json: - if icon not in new_icons: - new_icons.append(icon) - return new_icons - - -def optimize_svgs(new_icons: List[str], icons_folder_path: str, logfile: FileIO): +def optimize_svgs(new_icons: List[dict], icons_folder_path: str, logfile: FileIO): """ Optimize the newly added svgs. This is done in batches since the command line has a limit on characters allowed. @@ -118,7 +99,7 @@ def optimize_svgs(new_icons: List[str], icons_folder_path: str, logfile: FileIO) subprocess.run(["npm", "run", "optimize-svg", "--", f"--svgFiles={json.dumps(batch)}"], shell=True) -def update_icomoon_json(new_icons: List[str], icomoon_json_path: str, logfile: FileIO): +def update_icomoon_json(new_icons: List[dict], icomoon_json_path: str, logfile: FileIO): """ Update the `icomoon.json` if it contains any icons that needed to be updated. This will remove the icons @@ -129,7 +110,7 @@ def update_icomoon_json(new_icons: List[str], icomoon_json_path: str, logfile: F cur_len = len(icomoon_json["icons"]) messages = [] - wrapper_function = lambda icomoon_icon : find_icomoon_icon_not_in_new_icons( + wrapper_function = lambda icomoon_icon: find_icomoon_icon_not_in_new_icons( icomoon_icon, new_icons, messages) icons_to_keep = filter(wrapper_function, icomoon_json["icons"]) icomoon_json["icons"] = list(icons_to_keep) @@ -137,7 +118,7 @@ def update_icomoon_json(new_icons: List[str], icomoon_json_path: str, logfile: F new_len = len(icomoon_json["icons"]) print(f"Update completed. Removed {cur_len - new_len} icons:", *messages, sep='\n', file=logfile) filehandler.write_to_file(icomoon_json_path, json.dumps(icomoon_json)) - + def find_icomoon_icon_not_in_new_icons(icomoon_icon: Dict, new_icons: List, messages: List): """ @@ -145,7 +126,7 @@ def find_icomoon_icon_not_in_new_icons(icomoon_icon: Dict, new_icons: List, mess This also add logging for which icons were removed. :param icomoon_icon - a dict object from the icomoon.json's `icons` attribute. :param new_icons - a list of new icons. Each element is an object from the `devicon.json`. - :param messages - an empty list where the function can attach logging on which + :param messages - an empty list where the function can attach logging on which icon were removed. """ for new_icon in new_icons: @@ -182,7 +163,7 @@ def get_release_message(token, logfile: FileIO): 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, + finalString = "{0}\n\n {1}\n{2}\n\n {3}\n{4}".format(thankYou, iconTitle, "\n".join(newIcons), featureTitle, "\n".join(features)) diff --git a/.github/scripts/icomoon_peek.py b/.github/scripts/icomoon_peek.py index 7d66d1117..2abf7d136 100644 --- a/.github/scripts/icomoon_peek.py +++ b/.github/scripts/icomoon_peek.py @@ -5,29 +5,31 @@ def main(): runner = None try: - args = arg_getters.get_selenium_runner_args(has_token=False, peek_mode=True) + args = arg_getters.get_selenium_runner_args(has_token=False) all_icons = filehandler.get_json_file_content(args.devicon_json_path) - # get only the icon object that has the name matching the pr title - filtered_icon = util.find_object_added_in_pr(all_icons, args.pr_title) - svgs = filehandler.get_svgs_paths([filtered_icon], args.icons_folder_path, True) - screenshot_folder = filehandler.create_screenshot_folder("./") + # get only the icons that were changed in this PR + filtered_icons = util.find_changed_icons(all_icons, args.changed_files) + svgs = filehandler.get_svgs_paths(filtered_icons, args.icons_folder_path, True) + screenshot_folder = filehandler.create_screenshot_folder("./") runner = PeekSeleniumRunner(args.download_path, args.geckodriver_path, args.headless) - svgs_with_strokes = runner.peek(svgs, screenshot_folder, filtered_icon) - print("Task completed.") - 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" + for filtered_icon in filtered_icons: + svgs_with_strokes = runner.peek(svgs, screenshot_folder, filtered_icon) + print(f"Task completed for {filtered_icon}.") + + 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: if runner is not None: - runner.close() + runner.close() if __name__ == "__main__": diff --git a/.github/workflows/build_icons.yml b/.github/workflows/build_icons.yml index e5741d0ce..d49a981d1 100644 --- a/.github/workflows/build_icons.yml +++ b/.github/workflows/build_icons.yml @@ -6,6 +6,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: '0' + fetch-tags: 'true' + - uses: actions/setup-python@v5 with: python-version: '3.10' @@ -20,10 +24,28 @@ jobs: shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: > - python ./.github/scripts/icomoon_build.py - ./.github/scripts/build_assets/geckodriver-v0.32.2-linux64/geckodriver ./icomoon.json - ./devicon.json ./icons ./ $GITHUB_TOKEN --headless + run: | + echo "Getting the latest tags" + git fetch --tags + LATEST_TAG=$(git describe --tags --abbrev=0) + echo "Latest tag: $LATEST_TAG" + SECOND_LATEST_TAG=$(git describe --tags --abbrev=0 $LATEST_TAG^) + echo "Second latest tag: $SECOND_LATEST_TAG" + + echo "Getting the changed icons" + CHANGED_ICONS=$(git diff --name-only $SECOND_LATEST_TAG..$LATEST_TAG | grep -E 'icons/.*\.svg') + echo "Changed icons: $CHANGED_ICONS" + + echo "Building the icons" + python ./.github/scripts/icomoon_build.py \ + ./.github/scripts/build_assets/geckodriver-v0.32.2-linux64/geckodriver \ + ./icomoon.json \ + ./devicon.json \ + ./icons \ + ./ \ + $GITHUB_TOKEN \ + --headless \ + ${CHANGED_ICONS} - name: Upload geckodriver.log for debugging purposes uses: actions/upload-artifact@v4 diff --git a/.github/workflows/check_icon_pr.yml b/.github/workflows/check_icon_pr.yml index 27e5cae47..3ebeb084f 100644 --- a/.github/workflows/check_icon_pr.yml +++ b/.github/workflows/check_icon_pr.yml @@ -27,9 +27,10 @@ jobs: - name: Run the check_svg script if: ${{ !env.wrong_branch }} - env: - PR_TITLE: ${{ github.event.pull_request.title }} - run: python ./.github/scripts/check_icon_pr.py "$PR_TITLE" ./icons ./devicon.json + run: | + git fetch origin ${{ github.base_ref }} + CHANGED_ICONS=$(git diff --name-only origin/${{ github.base_ref }} ${{ github.sha }} | grep -E 'icons/.*\.svg') + python ./.github/scripts/check_icon_pr.py ./icons ./devicon.json ${CHANGED_ICONS} - name: Upload the err messages uses: actions/upload-artifact@v4 diff --git a/.github/workflows/peek_icons.yml b/.github/workflows/peek_icons.yml index a337d5543..50bc5cda8 100644 --- a/.github/workflows/peek_icons.yml +++ b/.github/workflows/peek_icons.yml @@ -39,10 +39,17 @@ jobs: env: PR_TITLE: ${{ github.event.pull_request.title }} shell: bash - run: > - python ./.github/scripts/icomoon_peek.py - ./.github/scripts/build_assets/geckodriver-v0.32.2-linux64/geckodriver ./icomoon.json - ./devicon.json ./icons ./ --headless "$PR_TITLE" + run: | + git fetch origin ${{ github.base_ref }} + CHANGED_ICONS=$(git diff --name-only origin/${{ github.base_ref }} ${{ github.sha }} | grep -E 'icons/.*\.svg') + python ./.github/scripts/icomoon_peek.py \ + ./.github/scripts/build_assets/geckodriver-v0.32.2-linux64/geckodriver \ + ./icomoon.json \ + ./devicon.json \ + ./icons \ + ./ \ + --headless \ + ${CHANGED_ICONS} - name: Upload the err messages (created by icomoon_peek.py) uses: actions/upload-artifact@v4