Skip to content
This repository was archived by the owner on Jun 26, 2026. It is now read-only.

Labels

Labels #8

Workflow file for this run

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