Skip to content

Record settings_version on each repo, so "which repos are behind" is a query - #55

Merged
gavinbee merged 3 commits into
mainfrom
53-settings-version-property
Sep 6, 2026
Merged

Record settings_version on each repo, so "which repos are behind" is a query#55
gavinbee merged 3 commits into
mainfrom
53-settings-version-property

Conversation

@gavinbee

@gavinbee gavinbee commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #53

What & why

The job summary from #51 says what one rollout did. Nothing on a repo said what state it was in, so "which repos are behind" meant reading run logs and trusting no repo had been created since. That gap is how officials-admin sat unreconciled from creation until someone noticed by hand — the failure that opened #34.

With --version, apply-settings.py now PATCHes the tag onto each repo as the settings_version repository custom property. The org-wide values endpoint then answers the question in one call:

export WANT=$(gh release view -R swimblocks/.github --json tagName --jq .tagName)
gh api orgs/swimblocks/properties/values --paginate \
  --jq '.[] | select(([.properties[]
        | select(.property_name == "settings_version") | .value] | first) != env.WANT)
        | .repository_full_name'

Per repo, not the org batch endpoint. PATCH /repos/{owner}/{repo}/properties/values is the entirety of what the repository-level Custom properties permission grants — one endpoint, nothing else. PATCH /orgs/{org}/properties/values would do all seven repos in a single call but rides on the organization Custom properties permission, which also carries create, update and delete of every property definition in the org. That is the same test #51 applied when it put the releaser in its own app rather than widening the reconciler, so seven calls it is.

Only a repo that came through clean is stamped. The value means "this repo was taken through the whole of settings.yml at this tag", so a repo with drift left on it must not claim it — leaving it unstamped is what keeps it in the query's output. Runs without --version stamp nothing: create-repo.sh applies whatever is on main, and there is no release to name.

A failed write fails the run rather than skipping the way branch protection does. Every cause — property not defined, permission not granted, values_editable_by locking out the repository endpoint — is a setup gap nobody should sit in, so red is the right signal. It does mean the setup below has to be in place before merge, not after; it is.

Also fixed: the permission table was wrong

It listed Metadata read and Administration write. The live installation has held issues: write since label reconciliation landed in #48, undocumented:

gh api orgs/swimblocks/installations --jq '.installations[] | {app_slug, permissions}'
{"app_slug":"swimblocks-reconciler","permissions":{"administration":"write","issues":"write","metadata":"read"}}

Rather than add a row beside a wrong one, the table is now permission → endpoint → reason, so the next addition has somewhere to go.

Setup this depends on

rollout.yml fails on every repo without it. Both are done on swimblocks and the runbook in docs/reconciler.md carries them for the next org:

  1. Define settings_version at org level — string, required=false, no default (a default is reported for repos that were never reconciled and reads as an all-clear), and values_editable_by=org_and_repo_actors. That last one is not optional: it defaults to org_actors, which is org owners and property managers only, and the reconciler writes through the repository endpoint as a repo actor. The 403 it would produce is indistinguishable from a missing permission.
  2. Grant swimblocks-reconciler Custom properties: Read & write, and accept the installation request. Editing an app's permissions changes nothing about its tokens until an org owner accepts; that is the usual reason a freshly granted permission still 403s.

Live state, for the record:

gh api orgs/swimblocks/properties/schema/settings_version
  value_type=string  required=false  values_editable_by=org_and_repo_actors

gh api orgs/swimblocks/installations
  swimblocks-reconciler: administration=write issues=write metadata=read
                         repository_custom_properties=write

Testing

  • ruff check . clean; pytest -q 39 passed (5 new: payload shape, the property name the query depends on, the PATCH endpoint and body, failure reporting, stderr capture).
  • The drift query above was run against the live org and correctly listed all 7 repos as behind, nothing being stamped yet.
  • The reconciler's live permissions were read back from orgs/swimblocks/installations, which is where the issues: write discrepancy came from.
  • The property definition and the app's granted permissions were both read back after setup, and are quoted above.
  • Not exercised end-to-end. Writing the value from a local gh proves nothing about the app: an org-owner token satisfies values_editable_by whatever it is set to, so it cannot distinguish the repo-actor path the installation token takes. The first real proof is the rollout after merge — expect OK settings_version: <tag> per repo, and the drift query returning nothing.

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

🤖 Generated with Claude Code

gavinbee and others added 3 commits September 6, 2026 11:01
The job summary says what one run did; nothing on a repo said what state
it was in, so "which repos are behind" meant reading run logs and trusting
that no repo had been created since. That gap is how officials-admin sat
unreconciled from creation until someone noticed by hand.

With --version, apply-settings.py now PATCHes the tag onto each repo as the
settings_version repository custom property, so the org-wide values endpoint
answers the question in one call.

Per repo rather than the org-level batch endpoint: PATCH /repos/{repo}/
properties/values is the entirety of what the repository-level Custom
properties permission grants, while PATCH /orgs/{org}/properties/values
rides on the organization permission, which also carries create, update and
delete of every property definition in the org. Same test that put the
releaser in its own app instead of widening the reconciler.

Only a repo that came through clean is stamped: the value means the repo
was taken through the whole of settings.yml at that tag, so one with drift
left on it must not claim it — that is what keeps it in the drift query's
output. Runs without --version stamp nothing, since create-repo.sh applies
whatever is on main and there is no release to name.

Closes #53

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgLE8cg9huy2EZnRoXhmwp
…missions

Adds the one-time org-owner command that defines settings_version, the
copy-paste "which repos are behind" query, and troubleshooting rows for the
two setup gaps that make the write fail (property undefined, permission not
granted or not yet accepted on the installation).

The permission table was also wrong before this. It listed Metadata read and
Administration write; the installation has held Issues write since label
reconciliation landed in #48, undocumented. The table is now a permission ->
endpoint -> reason mapping so a future addition has somewhere to go.

Two things worth having written down that cost time to establish: editing a
GitHub App's permissions does not change what its tokens can do until an org
owner accepts the installation request, and gh's --jq is gojq, so the query
uses env.WANT rather than needing a separate jq binary.

0003 records the property as part of version traceability rather than an open
item, including why the definition stays an org-owner action and why it
carries no default value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgLE8cg9huy2EZnRoXhmwp
…lout works

The property defaults to values_editable_by=org_actors, which is org owners and
property managers only. The reconciler holds the repository Custom properties
permission and writes through PATCH /repos/{repo}/properties/values, so it acts
as a repo actor and that default shuts it out with a 403 that looks identical
to a missing permission.

The define command now sets org_and_repo_actors, the troubleshooting row for a
403 lists all three causes, and the description matches the live property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgLE8cg9huy2EZnRoXhmwp
@gavinbee
gavinbee marked this pull request as ready for review September 6, 2026 15:43
@gavinbee
gavinbee merged commit 320c1f5 into main Sep 6, 2026
1 check passed
@gavinbee
gavinbee deleted the 53-settings-version-property branch September 6, 2026 15:50
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.

Record settings_version on each repo, so "which repos are behind" is a query

1 participant