Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
29c0117
chore: migrate to trunk-based development
Snailedlt Mar 7, 2026
2e70aba
feat: automate build, merge, and release on PR approval
Snailedlt Mar 7, 2026
0091257
feat: add nightly build workflow and fix peek screenshot PR num path
Snailedlt Mar 8, 2026
08f7531
chore: finalize trunk-based workflow refactor
Snailedlt May 16, 2026
bdebed4
fix: correct pr_num artifact path in post_peek_screenshot workflow
Snailedlt May 16, 2026
3568f40
refactor: replace Imgur with self-hosted screenshots branch
Snailedlt May 16, 2026
71ba312
new icon: claude (original, plain) (#1)
Snailedlt May 16, 2026
b43e07b
chore: add comment to trigger workflow re-indexing
Snailedlt May 16, 2026
11a2180
fix: use correct range when no previous build commit exists
Snailedlt May 16, 2026
df0f87e
fix: fall back to last release tag when no build commit exists
Snailedlt May 16, 2026
8d3c3ac
fix: install Firefox + chmod geckodriver, auto-create build-failed label
Snailedlt May 16, 2026
513947b
debug: add log.txt artifact upload to nightly build
Snailedlt May 16, 2026
0920856
fix: gracefully handle API failures in get_icons_for_building
Snailedlt May 16, 2026
a00a460
fix: use browser-actions/setup-firefox and icomoon_build_githubless.py
Snailedlt May 16, 2026
03b1590
debug: add screenshots + fallback selector to SeleniumRunner
Snailedlt May 16, 2026
afe18e3
fix: use full-page screenshot to avoid canvas taint error
Snailedlt May 16, 2026
4e8154c
fix: increase icomoon.io fallback page-load timeout to 90s
Snailedlt May 16, 2026
193d277
build: generate font files and CSS
github-actions[bot] May 16, 2026
4ebf52c
chore: bump version to 2.17.1
github-actions[bot] May 16, 2026
cc94b12
new icon: vlang (original, plain) (#11)
Snailedlt May 16, 2026
275ce10
fix: make icomoon hamburger menu operations non-fatal in build
Snailedlt May 16, 2026
1468c09
build: generate font files and CSS
github-actions[bot] May 16, 2026
ccea040
chore: bump version to 2.17.2
github-actions[bot] May 16, 2026
69b07f7
fix: remove deselect_all from build flow to prevent empty font genera…
Snailedlt May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE/new_icon.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- Tick the checkboxes to ensure you've done everything correctly -->
- [ ] PR does not match another non-stale PR currently opened
- [ ] PR name matches the format *new icon: <i>Icon name</i> (<i>versions separated by comma</i>)*. More details [here](https://github.com/devicons/devicon/wiki/Overview-on-Submitting-Icons)
- [ ] PR's base is the `develop` branch.
- [ ] PR's base is the `master` branch.
- [ ] Your icons are inside a folder as seen [here](https://github.com/devicons/devicon/wiki/Organizing-SVGs)
- [ ] SVG matches the standards laid out [here](https://github.com/devicons/devicon/wiki/SVG-Standards)
- [ ] A new object is added in the `devicon.json` file at the correct alphabetic position as seen [here](https://github.com/devicons/devicon/wiki/Updating-%60devicon.json%60)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ 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()
# Note: intentionally NOT calling deselect_all_icons_in_top_set() here.
# Deselecting then failing to re-select leaves 0 icons selected → empty font.
# Icons loaded from icomoon.json and newly uploaded SVGs are selected by default.
self.upload_svgs(svgs, screenshot_folder)
self.take_icon_screenshot(screenshot_folder)
self.download_icomoon_fonts(zip_path)
Expand Down Expand Up @@ -91,9 +91,12 @@ def upload_svgs(self, svgs: List[str], screenshot_folder: str):
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.select_all_icons_in_top_set()
# try to select the latest icons for a cleaner screenshot, but non-fatal
try:
self.switch_toolbar_option(IcomoonOptionState.SELECT)
self.select_all_icons_in_top_set()
except Exception as e:
print(f"Warning: could not select icons for screenshot (non-fatal): {e}", file=self.log_output)
new_svgs_path = str(Path(screenshot_folder, "new_svgs.png").resolve())
self.driver.save_screenshot(new_svgs_path)

Expand All @@ -117,7 +120,8 @@ def take_icon_screenshot(self, screenshot_folder: str):

# wait a bit for all the icons to load before we take a pic
time.sleep(SeleniumRunner.MED_WAIT_IN_SEC)
main_content.screenshot(new_icons_path)
# Use full-page screenshot to avoid canvas taint restrictions
self.driver.save_screenshot(new_icons_path)
print("Saved screenshot of the new icons...", file=self.log_output)

def go_to_generate_font_page(self):
Expand Down Expand Up @@ -145,7 +149,10 @@ def download_icomoon_fonts(self, zip_path: Path):
if self.current_page != IcomoonPage.SELECTION:
self.go_to_page(IcomoonPage.SELECTION)

self.select_all_icons_in_top_set()
try:
self.select_all_icons_in_top_set()
except Exception as e:
print(f"Warning: could not select all icons (non-fatal, icons may already be selected): {e}", file=self.log_output)
self.go_to_generate_font_page()

download_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
Expand Down
28 changes: 23 additions & 5 deletions .github/scripts/build_assets/selenium_runner/SeleniumRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,29 @@ def set_browser_options(self, download_path: str, geckodriver_path: str,
self.driver = self.create_driver_instance(options, geckodriver_path)

self.driver.get(self.ICOMOON_URL)
# 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", file=self.log_output)

# wait until the whole web page is loaded
# Try original selector first, fall back to a more generic check
try:
WebDriverWait(self.driver, self.MED_WAIT_IN_SEC).until(
ec.element_to_be_clickable((By.XPATH, "(//i[@class='icon-menu'])[2]"))
)
print("Accessed icomoon.io (via icon-menu selector)", file=self.log_output)
except SeleniumTimeoutException:
print("icon-menu selector not found, trying fallback selectors...", file=self.log_output)
# Fallback: wait up to 90s for any file input (the core upload mechanism)
# icomoon.io can be slow to initialize the Angular app
try:
WebDriverWait(self.driver, 90).until(
ec.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']"))
)
print("Accessed icomoon.io (via file input fallback)", file=self.log_output)
except SeleniumTimeoutException:
self.driver.save_screenshot("./screenshots/icomoon_load_failed.png")
raise Exception(
f"Could not load icomoon.io after 90s. Page title: '{self.driver.title}'. "
"The page may have changed structure or blocked headless browsing."
)

def create_driver_instance(self, options: Options, geckodriver_path: str):
"""
Expand Down
15 changes: 9 additions & 6 deletions .github/scripts/icomoon_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,17 @@ def get_icons_for_building(icomoon_json_path: str, devicon_json_path: str, token
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)
try:
pull_reqs = api_handler.get_merged_pull_reqs_since_last_release(token, logfile)
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)
except Exception as e:
print(f"Warning: could not fetch PRs from GitHub API: {e}. Falling back to devicon.json diff.", file=logfile)

# 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
Expand Down
24 changes: 0 additions & 24 deletions .github/scripts/in_develop_labeler.py

This file was deleted.

101 changes: 101 additions & 0 deletions .github/workflows/auto_build_merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Auto Build and Merge

on:
pull_request_review:
types: [submitted]

permissions:
contents: write
pull-requests: write
issues: write

jobs:
auto-merge:
name: Auto Merge Icon PR
runs-on: ubuntu-latest
# Only act on approvals targeting master from the base repo (not forks)
if: >
github.event.review.state == 'approved' &&
github.event.pull_request.base.ref == 'master'

steps:
- name: Check if PR modifies icon files
id: check-files
uses: actions/github-script@v7
with:
script: |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});

const hasIconChanges = files.some(file =>
file.filename.startsWith('icons/') || file.filename === 'devicon.json'
);

console.log('Has icon changes:', hasIconChanges);
core.setOutput('has_icon_changes', String(hasIconChanges));

if (!hasIconChanges) {
console.log('PR does not modify icon files — skipping auto-merge');
}

- name: Check approval count
if: steps.check-files.outputs.has_icon_changes == 'true'
id: check-approvals
uses: actions/github-script@v7
with:
script: |
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});

// Latest review per user wins
const reviewsByUser = {};
for (const review of reviews) {
if (
!reviewsByUser[review.user.login] ||
new Date(review.submitted_at) > new Date(reviewsByUser[review.user.login].submitted_at)
) {
reviewsByUser[review.user.login] = review;
}
}

const approvals = Object.values(reviewsByUser).filter(r => r.state === 'APPROVED');
console.log('Approval count:', approvals.length);
core.setOutput('approval_count', String(approvals.length));

- name: Squash merge PR
if: >
steps.check-files.outputs.has_icon_changes == 'true' &&
steps.check-approvals.outputs.approval_count >= 1
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;

try {
await github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
merge_method: 'squash',
commit_title: `${pr.title} (#${pr.number})`,
commit_message: pr.body || ''
});
console.log(`PR #${pr.number} squash-merged successfully`);
} catch (err) {
console.error('Merge failed:', err.message);
// Comment on PR to alert author and approver
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `@${pr.user.login} Auto-merge failed. Please check the [workflow run](${runUrl}) for details.`
});
core.setFailed('Merge failed: ' + err.message);
}
Loading
Loading