Skip to content

Commit d60b334

Browse files
Thomas-BoiamacadoPanquesito7
authored
Build bot now build new SVGs in folder that were already built (#666)
* Refactor the pull request fetching code * Refactor build script to use past PRs * Added function to update icomoon json * new icon: matlab (line) (#640) * Add matlab-line * Fixed issues reported by check svg bot * optimisation for svg (#643) Co-authored-by: Clemens Bastian <8781699+amacado@users.noreply.github.com> Co-authored-by: David Leal <halfpacho@gmail.com> * Add better logging to icomoon_build Co-authored-by: Clemens Bastian <8781699+amacado@users.noreply.github.com> Co-authored-by: David Leal <halfpacho@gmail.com>
1 parent 8d617d7 commit d60b334

8 files changed

Lines changed: 253 additions & 121 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import requests
2+
import sys
3+
import re
4+
5+
def get_merged_pull_reqs(token, page):
6+
"""
7+
Get the merged pull requests based on page. There are
8+
100 results page. See https://docs.github.com/en/rest/reference/pulls
9+
for more details on the parameters.
10+
:param token, a GitHub API token.
11+
:param page, the page number.
12+
"""
13+
queryPath = "https://api.github.com/repos/devicons/devicon/pulls"
14+
headers = {
15+
"Authorization": f"token {token}"
16+
}
17+
params = {
18+
"accept": "application/vnd.github.v3+json",
19+
"state": "closed",
20+
"per_page": 100,
21+
"page": page
22+
}
23+
24+
print(f"Querying the GitHub API for requests page #{page}")
25+
response = requests.get(queryPath, headers=headers, params=params)
26+
if not response:
27+
print(f"Can't query the GitHub API. Status code is {response.status_code}. Message is {response.text}")
28+
sys.exit(1)
29+
30+
closed_pull_reqs = response.json()
31+
return [merged_pull_req
32+
for merged_pull_req in closed_pull_reqs
33+
if merged_pull_req["merged_at"] is not None]
34+
35+
36+
def is_feature_icon(pull_req_data):
37+
"""
38+
Check whether the pullData is a feature:icon PR.
39+
:param pull_req_data - the data on a specific pull request from GitHub.
40+
:return true if the pullData has a label named "feature:icon"
41+
"""
42+
for label in pull_req_data["labels"]:
43+
if label["name"] == "feature:icon":
44+
return True
45+
return False
46+
47+
48+
def find_all_authors(pull_req_data, token):
49+
"""
50+
Find all the authors of a PR based on its commits.
51+
:param pull_req_data - the data on a specific pull request from GitHub.
52+
:param token - a GitHub API token.
53+
"""
54+
headers = {
55+
"Authorization": f"token {token}"
56+
}
57+
response = requests.get(pull_req_data["commits_url"], headers=headers)
58+
if not response:
59+
print(f"Can't query the GitHub API. Status code is {response.status_code}")
60+
print("Response is: ", response.text)
61+
return
62+
63+
commits = response.json()
64+
authors = set() # want unique authors only
65+
for commit in commits:
66+
authors.add(commit["commit"]["author"]["name"])
67+
return ", ".join(["@" + author for author in list(authors)])
68+
69+
70+
def get_merged_pull_reqs_since_last_release(token):
71+
"""
72+
Get all the merged pull requests since the last release.
73+
"""
74+
stopPattern = r"^(r|R)elease v"
75+
pull_reqs = []
76+
found_last_release = False
77+
page = 1
78+
79+
print("Getting PRs since last release.")
80+
while not found_last_release:
81+
data = get_merged_pull_reqs(token, page)
82+
# assume we don't encounter it during the loop
83+
last_release_index = 101
84+
85+
for i in range(len(data)):
86+
if re.search(stopPattern, data[i]["title"]):
87+
found_last_release = True
88+
last_release_index = i
89+
break
90+
pull_reqs.extend(data[:last_release_index])
91+
page += 1
92+
93+
# should contain all the PRs since last release
94+
return pull_reqs

.github/scripts/build_assets/arg_getters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def get_selenium_runner_args(peek_mode=False):
3333
help="The download destination of the Icomoon files",
3434
action=PathResolverAction)
3535

36+
parser.add_argument("token",
37+
help="The GitHub token to access the GitHub REST API.",
38+
type=str)
39+
3640
if peek_mode:
3741
parser.add_argument("--pr_title",
3842
help="The title of the PR that we are peeking at")

.github/scripts/build_assets/util.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import os
2+
import re
3+
from typing import List
24
import platform
35
import sys
46
import traceback
@@ -41,4 +43,22 @@ def set_env_var(key: str, value: str, delimiter: str='~'):
4143
else:
4244
os.system(f'echo "{key}={value}" >> $GITHUB_ENV')
4345
else:
44-
raise Exception("This function doesn't support this platform: " + platform.system())
46+
raise Exception("This function doesn't support this platform: " + platform.system())
47+
48+
49+
def find_object_added_in_this_pr(icons: List[dict], pr_title: str):
50+
"""
51+
Find the icon name from the PR title.
52+
:param icons, a list of the font objects found in the devicon.json.
53+
:pr_title, the title of the PR that this workflow was called on.
54+
:return a dictionary with the "name"
55+
entry's value matching the name in the pr_title.
56+
:raise If no object can be found, raise an Exception.
57+
"""
58+
try:
59+
pattern = re.compile(r"(?<=^new icon: )\w+ (?=\(.+\))", re.I)
60+
icon_name = pattern.findall(pr_title)[0].lower().strip() # should only have one match
61+
icon = [icon for icon in icons if icon["name"] == icon_name][0]
62+
return icon
63+
except IndexError: # there are no match in the findall()
64+
raise Exception("Couldn't find an icon matching the name in the PR title.")
Lines changed: 32 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,39 @@
11
import requests
2-
from build_assets import arg_getters
2+
from build_assets import arg_getters, api_handler, util
33
import re
44

55
def main():
6-
print("Please wait a few seconds...")
7-
args = arg_getters.get_release_message_args()
8-
queryPath = "https://api.github.com/repos/devicons/devicon/pulls?accept=application/vnd.github.v3+json&state=closed&per_page=100"
9-
stopPattern = r"^(r|R)elease v"
10-
headers = {
11-
"Authorization": f"token {args.token}"
12-
}
13-
14-
response = requests.get(queryPath, headers=headers)
15-
if not response:
16-
print(f"Can't query the GitHub API. Status code is {response.status_code}. Message is {response.text}")
17-
return
18-
19-
data = response.json()
20-
newIcons = []
21-
features = []
22-
23-
for pullData in data:
24-
if re.search(stopPattern, pullData["title"]):
25-
break
26-
27-
authors = findAllAuthors(pullData, headers)
28-
markdown = f"- [{pullData['title']}]({pullData['html_url']}) by {authors}."
29-
30-
if isFeatureIcon(pullData):
31-
newIcons.append(markdown)
32-
else:
33-
features.append(markdown)
34-
35-
thankYou = "A huge thanks to all our maintainers and contributors for making this release possible!"
36-
iconTitle = "**{} New Icons**\n".format(len(newIcons))
37-
featureTitle = "**{} New Features**\n".format(len(features))
38-
finalString = "{0}\n\n {1}{2}\n\n {3}{4}".format(thankYou,
39-
iconTitle, "\n".join(newIcons), featureTitle, "\n".join(features))
40-
41-
print("--------------Here is the build message--------------\n", finalString)
42-
43-
44-
"""
45-
Check whether the pullData is a feature:icon PR.
46-
:param pullData
47-
:return true if the pullData has a label named "feature:icon"
48-
"""
49-
def isFeatureIcon(pullData):
50-
for label in pullData["labels"]:
51-
if label["name"] == "feature:icon":
52-
return True
53-
return False
54-
55-
56-
"""
57-
Find all the authors of a PR based on its commits.
58-
:param pullData - the data of a pull request.
59-
"""
60-
def findAllAuthors(pullData, authHeader):
61-
response = requests.get(pullData["commits_url"], headers=authHeader)
62-
if not response:
63-
print(f"Can't query the GitHub API. Status code is {response.status_code}")
64-
print("Response is: ", response.text)
65-
return
66-
67-
commits = response.json()
68-
authors = set() # want unique authors only
69-
for commit in commits:
70-
authors.add("@" + commit["author"]["login"])
71-
return ", ".join(list(authors))
72-
6+
try:
7+
print("Please wait a few seconds...")
8+
args = arg_getters.get_release_message_args()
9+
10+
# fetch first page by default
11+
data = api_handler.get_merged_pull_reqs_since_last_release(args.token)
12+
newIcons = []
13+
features = []
14+
15+
print("Parsing through the pull requests")
16+
for pullData in data:
17+
authors = api_handler.find_all_authors(pullData, args.token)
18+
markdown = f"- [{pullData['title']}]({pullData['html_url']}) by {authors}."
19+
20+
if api_handler.is_feature_icon(pullData):
21+
newIcons.append(markdown)
22+
else:
23+
features.append(markdown)
24+
25+
print("Constructing message")
26+
thankYou = "A huge thanks to all our maintainers and contributors for making this release possible!"
27+
iconTitle = f"**{len(newIcons)} New Icons**"
28+
featureTitle = f"**{len(features)} New Features**"
29+
finalString = "{0}\n\n {1}\n{2}\n\n {3}\n{4}".format(thankYou,
30+
iconTitle, "\n".join(newIcons), featureTitle, "\n".join(features))
31+
32+
print("--------------Here is the build message--------------\n", finalString)
33+
print("Script finished")
34+
except Exception as e:
35+
util.exit_with_err(e)
36+
7337

7438
if __name__ == "__main__":
7539
main()

.github/scripts/icomoon_build.py

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
from pathlib import Path
22
import sys
33
from selenium.common.exceptions import TimeoutException
4+
import re
45
import subprocess
56
import json
7+
from typing import List, Dict
8+
69

710
# pycharm complains that build_assets is an unresolved ref
811
# don't worry about it, the script still runs
912
from build_assets.SeleniumRunner import SeleniumRunner
10-
from build_assets import filehandler, arg_getters
11-
from build_assets import util
13+
from build_assets import filehandler, arg_getters, util, api_handler
1214

1315

1416
def main():
15-
args = arg_getters.get_selenium_runner_args()
16-
new_icons = filehandler.find_new_icons(args.devicon_json_path, args.icomoon_json_path)
17-
if len(new_icons) == 0:
18-
sys.exit("No files need to be uploaded. Ending script...")
19-
20-
# print list of new icons
21-
print("List of new icons:", *new_icons, sep = "\n")
22-
17+
"""
18+
Build the icons using Icomoon. Also optimize the svgs.
19+
"""
2320
runner = None
2421
try:
25-
svgs = filehandler.get_svgs_paths(new_icons, args.icons_folder_path, icon_versions_only=False)
26-
# optimizes the files
27-
# do in each batch in case the command
28-
# line complains there's too many characters
29-
start = 0
30-
step = 10
31-
for i in range(start, len(svgs), step):
32-
batch = svgs[i:i + step]
33-
subprocess.run(["npm", "run", "optimize-svg", "--", f"--svgFiles={json.dumps(batch)}"], shell=True)
22+
args = arg_getters.get_selenium_runner_args()
23+
new_icons = get_icons_for_building(args.devicon_json_path, args.token)
24+
if len(new_icons) == 0:
25+
sys.exit("No files need to be uploaded. Ending script...")
26+
27+
print(f"There are {len(new_icons)} icons to be build. Here are they:", *new_icons, sep = "\n")
28+
29+
print("Begin optimizing files")
30+
optimize_svgs(new_icons, args.icons_folder_path)
31+
32+
print("Updating the icomoon json")
33+
update_icomoon_json(new_icons, args.icomoon_json_path)
3434

3535
icon_svgs = filehandler.get_svgs_paths(
3636
new_icons, args.icons_folder_path, icon_versions_only=True)
@@ -50,7 +50,79 @@ def main():
5050
except Exception as e:
5151
util.exit_with_err(e)
5252
finally:
53-
runner.close()
53+
if runner is not None:
54+
runner.close()
55+
56+
57+
def get_icons_for_building(devicon_json_path: str, token: str):
58+
"""
59+
Get the icons for building.
60+
:param devicon_json_path - the path to the `devicon.json`.
61+
:param token - the token to access the GitHub API.
62+
"""
63+
all_icons = filehandler.get_json_file_content(devicon_json_path)
64+
pull_reqs = api_handler.get_merged_pull_reqs_since_last_release(token)
65+
new_icons = []
66+
67+
for pull_req in pull_reqs:
68+
if api_handler.is_feature_icon(pull_req):
69+
filtered_icon = util.find_object_added_in_this_pr(all_icons, pull_req["title"])
70+
new_icons.append(filtered_icon)
71+
return new_icons
72+
73+
74+
def optimize_svgs(new_icons: List[str], icons_folder_path: str):
75+
"""
76+
Optimize the newly added svgs. This is done in batches
77+
since the command line has a limit on characters allowed.
78+
:param new_icons - the new icons that need to be optimized.
79+
:param icons_folder_path - the path to the /icons folder.
80+
"""
81+
svgs = filehandler.get_svgs_paths(new_icons, icons_folder_path, icon_versions_only=False)
82+
start = 0
83+
step = 10
84+
for i in range(start, len(svgs), step):
85+
batch = svgs[i:i + step]
86+
subprocess.run(["npm", "run", "optimize-svg", "--", f"--svgFiles={json.dumps(batch)}"], shell=True)
87+
88+
89+
def update_icomoon_json(new_icons: List[str], icomoon_json_path: str):
90+
"""
91+
Update the `icomoon.json` if it contains any icons
92+
that needed to be updated. This will remove the icons
93+
from the `icomoon.json` so the build script will reupload
94+
it later.
95+
"""
96+
icomoon_json = filehandler.get_json_file_content(icomoon_json_path)
97+
cur_len = len(icomoon_json["icons"])
98+
messages = []
99+
100+
wrapper_function = lambda icomoon_icon : find_icomoon_icon_not_in_new_icons(
101+
icomoon_icon, new_icons, messages)
102+
icons_to_keep = filter(wrapper_function, icomoon_json["icons"])
103+
icomoon_json["icons"] = list(icons_to_keep)
104+
105+
new_len = len(icomoon_json["icons"])
106+
print(f"Update completed. Removed {cur_len - new_len} icons:", *messages, sep='\n')
107+
filehandler.write_to_file(icomoon_json_path, json.dumps(icomoon_json))
108+
109+
110+
def find_icomoon_icon_not_in_new_icons(icomoon_icon: Dict, new_icons: List, messages: List):
111+
"""
112+
Find all the icomoon icons that are not listed in the new icons.
113+
This also add logging for which icons were removed.
114+
:param icomoon_icon - a dict object from the icomoon.json's `icons` attribute.
115+
:param new_icons - a list of new icons. Each element is an object from the `devicon.json`.
116+
:param messages - an empty list where the function can attach logging on which
117+
icon were removed.
118+
"""
119+
for new_icon in new_icons:
120+
pattern = re.compile(f"^{new_icon['name']}-")
121+
if pattern.search(icomoon_icon["properties"]["name"]):
122+
message = f"-'{icomoon_icon['properties']['name']}' cause it matches '{new_icon['name']}'"
123+
messages.append(message)
124+
return False
125+
return True
54126

55127

56128
if __name__ == "__main__":

0 commit comments

Comments
 (0)