Skip to content

Reconcile labels from settings.yml - #48

Merged
gavinbee merged 2 commits into
mainfrom
46-reconcile-labels
Sep 4, 2026
Merged

Reconcile labels from settings.yml#48
gavinbee merged 2 commits into
mainfrom
46-reconcile-labels

Conversation

@gavinbee

@gavinbee gavinbee commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #46

What & why

#47 added the labels: block to settings.yml as a documented-but-inert key: it named the canonical set, but apply-settings.py read only repository, branches and rulesets, so no-issue still had to be created by hand in every repo. This wires it up — the rest of #46.

apply-settings.py now 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.yml is 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.yml is 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_labels lists the labels again and diffs them against the YAML, rather than assuming the write took. That is deliberately not the shape of verify_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:

  • GitHub stores colours as six lowercase hex digits with no #, so #EDEDED in YAML and ededed from the API are the same colour.
  • The API returns null, not "", for a label with no description; that compares equal to an absent description in YAML.
  • A YAML entry with no color does not blank the colour already on the repo.

Label names are matched case-insensitively, since GitHub treats them that way — a No-Issue on the repo will not read as missing against a no-issue here.

list_labels pages with --paginate --jq '.[]', which emits one compact object per line; --paginate alone concatenates raw JSON arrays into something json.loads cannot read.

Testing

ruff check . clean. pytest -q22 passed, up from 11; the 11 new tests cover normalize_color and label_updates, both pure.

Dry-run against real repos, read-only, no writes:

swimblocks/deck-eval-gen: 12 labels; has no-issue = True
  -> 'no-issue' present; in sync
swimblocks/.github: 9 labels; has no-issue = False
  -> would CREATE 'no-issue'

The first case is the useful one: deck-eval-gen's no-issue was created by hand with gh 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-issue everywhere and the per-repo gh label create step goes away.

Checklist

  • Linked the issue this PR closes
  • Tests added/updated and passing
  • ruff check . clean (Python repos)
  • Docs/README updated if user-facing behaviour changed — settings.yml's comment now says the block is applied, and describes the additive rule

🤖 Generated with Claude Code

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
Comment thread .github/settings.yml Outdated
# 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

8ff34d2.

Comment thread scripts/apply-settings.py Outdated
# 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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']

8ff34d2.

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
@gavinbee
gavinbee merged commit 03f7a9b into main Sep 4, 2026
1 check passed
@gavinbee
gavinbee deleted the 46-reconcile-labels branch September 4, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document the no-issue PR exception in AGENTS.md and CONTRIBUTING.md

1 participant