Reconcile labels from settings.yml - #48
Conversation
The labels block landed in #47 as a documented-but-inert key: settings.yml named the canonical set, and apply-settings.py read only repository, branches and rulesets, so `no-issue` still had to be created by hand in each repo. This wires it up, which is the rest of #46. Reconciliation is additive on purpose. A label named in settings.yml is created when missing and corrected when its colour or description drifted; labels the YAML doesn't mention are left alone. Repos carry GitHub's defaults plus the ones Dependabot creates, and deleting a label strips it from every issue and PR that uses it — not something a scheduled job should do unprompted. settings.yml is the minimum set, not the whole set. Verification re-reads the labels and compares them, rather than assuming the write took. That is deliberately not the shape of verify_ruleset, which only checks that a ruleset name exists and is the subject of #45 — no reason to add a second instance of the same blind spot. Colour comparison normalises: GitHub stores six lowercase hex digits with no '#', so "#EDEDED" in YAML and "ededed" from the API are not drift. A null description from the API compares equal to an absent one in YAML, and an entry with no colour does not blank the colour already on the repo. Dry-run against real repos: deck-eval-gen's hand-created `no-issue` reads as in sync, and this repo's missing one reads as a create. Closes #46 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgLE8cg9huy2EZnRoXhmwp
| # colour or description. Reconciliation is **additive**: labels not listed here are left | ||
| # alone, because repos also carry GitHub's defaults and Dependabot's (`dependencies`, | ||
| # `python`), and deleting a label strips it from every issue and PR that uses it. This is | ||
| # the minimum set, not the whole set. |
There was a problem hiding this comment.
Why is there all this commentary about being applied by apply-settings.py ... we don't have that for other parts of this file that apply-settings applies?
There was a problem hiding this comment.
Agreed — the file header already says "Applied by scripts/apply-settings.py", and no other block repeats it. That line was a leftover from #47, where the block genuinely was inert and needed the "NOT YET APPLIED" caveat; once it stopped being inert I updated the caveat instead of deleting it.
Trimmed to the part that is actually non-obvious — that the values seed a new label rather than govern an existing one, and that unlisted labels are never removed:
# Labels that should exist in every repo. Created when missing and then left alone —
# the values here seed a new label, they don't govern an existing one. Labels not
# listed are untouched: repos carry GitHub's defaults and Dependabot's too, and
# deleting a label strips it from every issue and PR that used it.| # Labels | ||
| # | ||
| # Reconciliation here is **additive**: a label in `settings.yml` is created if | ||
| # missing and corrected if its colour or description drifted, but labels the |
There was a problem hiding this comment.
Should we really care if the color has drifted? Like what if a repo has 50 labels and adjusts colors to distinguish between them better? Do we care?
There was a problem hiding this comment.
No, we should not — and the fifty-label case is the right test. Someone recolouring to group labels visually is doing something useful, and a weekly job silently reverting it is churn and faintly hostile. Colour carries no policy weight. What does is that the label exists, since a PR cannot be given a label the repo lacks.
I took it further than colour: the same argument mostly covers description, so this is now create-if-missing and nothing else. settings.yml seeds a new label; the repo owns it afterwards. If we ever change the canonical description that is rare and deliberate, and better as a one-off than as update machinery carried for a case that may not arise.
Removes label_updates outright, and with it the false-drift handling it needed — #EDEDED vs ededed, a null description vs an absent one. normalize_color stays for the create path so a # in YAML never reaches the API.
One consequence worth naming, since the PR body made a point of it: verify_labels is now a presence check, which looks like verify_ruleset — the thing #45 criticises. The distinction holds, though. Verification should cover exactly what the tool asserts. This only ever creates, so presence is the whole assertion. verify_ruleset is different because the rules are the substance and it never looks at them.
Net 34 lines lighter. Dry-run still reads real repos correctly:
swimblocks/deck-eval-gen: 12 labels -> nothing to do
swimblocks/.github: 9 labels -> would create ['no-issue']
Review raised both of these. Colour drift is not worth reconciling. A repo that recolours its labels to group fifty of them visually is doing something useful, and a weekly job reverting that is churn. Colour carries no policy weight; what does is that the label exists, since a PR cannot be given a label the repo lacks. The same argument mostly covers description, so the simplest defensible rule is create-if-missing and nothing else: settings.yml seeds a new label, the repo owns it afterwards. If the canonical description ever changes, that is rare and deliberate, and better done as a one-off than by carrying update machinery for a case that may not arise. That removes label_updates entirely, along with the false-drift handling it needed for "#EDEDED" versus "ededed" and a null description versus an absent one. verify_labels reduces to a presence check — which is complete here, because presence is the whole assertion. That is what separates it from verify_ruleset, where the rules are the substance and go unchecked (#45); the point is that verification should cover exactly what the tool claims. normalize_color stays for the create path so a '#' in YAML never reaches the API. The settings.yml comment also restated "applied by apply-settings.py", which the file header already says and no other block repeats. Trimmed to the part that is actually non-obvious: labels are seeded, not governed, and unlisted ones are never removed. Net 34 lines lighter, and one fewer test than before for more behaviour covered. Refs #46 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgLE8cg9huy2EZnRoXhmwp
Closes #46
What & why
#47 added the
labels:block tosettings.ymlas a documented-but-inert key: it named the canonical set, butapply-settings.pyread onlyrepository,branchesandrulesets, sono-issuestill had to be created by hand in every repo. This wires it up — the rest of #46.apply-settings.pynow creates a label that is missing and corrects one whose colour or description drifted. Labels do not depend on visibility, so private repos get them too.Two decisions worth reviewing
Reconciliation is additive. A label named in
settings.ymlis created or corrected; labels the YAML does not mention are left alone. Repos carry GitHub's defaults plus the ones Dependabot creates (dependencies,python), and deleting a label strips it from every issue and PR that used it — not something a weekly scheduled job should do unprompted.settings.ymlis the minimum set, not the whole set. Making it authoritative and pruning is a different and much more destructive script; if that is wanted, it should be its own decision.Verification re-reads and compares.
verify_labelslists the labels again and diffs them against the YAML, rather than assuming the write took. That is deliberately not the shape ofverify_ruleset, which only checks that a ruleset name exists — the subject of #45. Adding a second instance of that blind spot while #45 is open seemed like the wrong move.Normalisation
Three ways a naive comparison would report false drift, all covered by tests:
#, so#EDEDEDin YAML andedededfrom the API are the same colour.null, not"", for a label with no description; that compares equal to an absent description in YAML.colordoes not blank the colour already on the repo.Label names are matched case-insensitively, since GitHub treats them that way — a
No-Issueon the repo will not read as missing against ano-issuehere.list_labelspages with--paginate --jq '.[]', which emits one compact object per line;--paginatealone concatenates raw JSON arrays into somethingjson.loadscannot read.Testing
ruff check .clean.pytest -q— 22 passed, up from 11; the 11 new tests covernormalize_colorandlabel_updates, both pure.Dry-run against real repos, read-only, no writes:
The first case is the useful one:
deck-eval-gen'sno-issuewas created by hand withgh label create, and the reconciler reads it as already in sync — so the normalisation and comparison agree with what GitHub actually stores.Once this merges, the next reconciler run creates
no-issueeverywhere and the per-repogh label createstep goes away.Checklist
ruff check .clean (Python repos)settings.yml's comment now says the block is applied, and describes the additive rule🤖 Generated with Claude Code