This repository was archived by the owner on Jun 26, 2026. It is now read-only.
Labels #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Labels | |
| on: | |
| pull_request: | |
| paths: | |
| - .github/labels.yml | |
| - .github/workflows/labels.yaml | |
| - .github/workflows/pr-labeler.yaml | |
| - .github/workflows/pr-size.yaml | |
| - .github/scripts/pr-labeler.js | |
| - .github/scripts/pr-labeler.test.js | |
| push: | |
| branches: [main] | |
| paths: | |
| - .github/labels.yml | |
| - .github/workflows/labels.yaml | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '17 4 * * 1' # Mondays at 04:17 UTC | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: labels-sync | |
| cancel-in-progress: false | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Validate labels.yml schema | |
| run: | | |
| python3 - <<'PY' | |
| import glob, re, sys, yaml | |
| path = '.github/labels.yml' | |
| data = yaml.safe_load(open(path)) | |
| errors = [] | |
| # 1. description ≤ 100 chars (GitHub REST API limit) | |
| for label in data: | |
| desc = label.get('description', '') or '' | |
| if len(desc) > 100: | |
| errors.append(f"{label['name']}: description {len(desc)} chars (max 100)") | |
| # 2. color is 6-char hex without leading # | |
| for label in data: | |
| color = label.get('color', '') or '' | |
| if not re.match(r'^[0-9A-Fa-f]{6}$', color): | |
| errors.append(f"{label['name']}: bad color {color!r} (must be 6-char hex without #)") | |
| # 3. unique top-level names | |
| names = [label['name'] for label in data] | |
| dups = sorted({n for n in names if names.count(n) > 1}) | |
| for n in dups: | |
| errors.append(f"duplicate name: {n}") | |
| # 4. aliases do not collide with any top-level name | |
| name_set = set(names) | |
| for label in data: | |
| for alias in (label.get('aliases') or []): | |
| if alias in name_set: | |
| errors.append(f"alias {alias!r} (under {label['name']!r}) collides with a top-level name") | |
| # 5. description-vs-automation drift: any label whose description | |
| # advertises automation ("auto-applied", "auto-closed", "auto-label*", | |
| # "auto-close*") must be referenced by name in at least one | |
| # automation file under .github/workflows/ or .github/scripts/, so | |
| # the description does not outlive the code that backs it. | |
| automation_files = sorted( | |
| glob.glob('.github/workflows/*.yaml') | |
| + glob.glob('.github/workflows/*.yml') | |
| + glob.glob('.github/scripts/*.js') | |
| + glob.glob('.github/scripts/*.cjs') | |
| + glob.glob('.github/scripts/*.mjs') | |
| ) | |
| automation_blob = '\n'.join(open(f).read() for f in automation_files) | |
| claim_pattern = re.compile(r'auto[- ](applied|closed|label[a-z]*|close[a-z]*)', re.I) | |
| for label in data: | |
| desc = label.get('description', '') or '' | |
| if claim_pattern.search(desc) and label['name'] not in automation_blob: | |
| errors.append( | |
| f"{label['name']}: description claims automation " | |
| f"({claim_pattern.search(desc).group(0)!r}) but the label name " | |
| f"is not referenced in .github/workflows/ or .github/scripts/ — " | |
| f"either wire the automation or drop the claim" | |
| ) | |
| if errors: | |
| for err in errors: | |
| print(f"::error::{err}") | |
| sys.exit(1) | |
| print(f"labels.yml schema OK ({len(data)} labels)") | |
| PY | |
| unit-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Run pr-labeler unit tests | |
| # Glob to test files only — pointing `node --test` at the directory | |
| # would treat the module under test as a test file and fail. The | |
| # grep guard catches the "passing because nothing ran" trap: a | |
| # zero-file glob exits 0 with `ℹ tests 0`, which would otherwise | |
| # green the build silently if the test file is moved or renamed. | |
| run: | | |
| output=$(node --test '.github/scripts/*.test.js') | |
| echo "$output" | |
| echo "$output" | grep -E '^[^ ]* tests [1-9]' \ | |
| || { echo "::error::no pr-labeler tests ran — check the glob in labels.yaml"; exit 1; } | |
| sync: | |
| needs: [validate, unit-tests] | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: EndBug/label-sync@v2 | |
| with: | |
| config-file: .github/labels.yml | |
| delete-other-labels: false |