Uh oh!
There was an error while loading. Please reload this page.
feat(ci): caller inventory and drift guard (backend#1415) - #131
Merged
Conversation
Nothing in the org detected a missing caller. merge-settings-drift.yml reads three booleans. kanban-reconcile.yml:436 probes a single filename to decide board scope, so a 403 there is indistinguishable from "repo not tracked". A caller that is simply absent produces no run, no annotation and no red check, and the absence of a signal is not a signal — which is how eight repos drifted unnoticed and how e2e-test-agent#1 closed without routing. What the guard now detects that nothing did: - a required caller missing from a repo, on the develop-first branch - a caller pinned to anything other than @main - a caller present where the inventory says exempt (a stale exemption) - a caller for a reusable the inventory has never heard of - add-to-kanban.yml / stale-backlog.yml drifted in content — they are copies, not callers, so no other mechanism would ever notice - a repo that joined the org, or left it, without the inventory following - a repo whose visibility or release-train membership contradicts the inventory, cross-checked against release-train/repos.yml repo-inventory.yml is the source of truth: every repo carries an entry for every reusable and every copy, and each is `required` or carries a written reason. A missing key is a guard failure, not a default; an exemption with no reason is rejected before any network call. Absence can never be implicit. Matching is on the resolved `uses:` value of a parsed workflow, never on filename. Two filename conventions are mixed within nearly every repo, kanban-closure-router's callers match neither, and code-quality.yml:60 is a commented-out example that a grep would count as a caller. Fail-closed throughout: a 403, a rate limit, a truncated git tree, an unparseable workflow, an unreadable inventory or an empty org listing each produce a failure, never an all-clear. scripts/tests/caller-drift-selftest.py asserts all of that offline with a stubbed gh, and the audit job needs it. The sync job that opens PRs for missing callers is deliberately deferred. Refs tracebloc/backend#1415, tracebloc/backend#1408 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The inventory is read from the checkout, but every repo's caller state - including tracebloc/.github's own - is read from its audit branch over the API. A PR that adds a caller to .github and flips that entry to `required` in the same commit therefore fails, because the caller is not on develop yet. That is the fail-closed direction, and reading .github's workflows from the checkout instead would break the develop-first policy on a run triggered from main. Documented rather than papered over, with the remedy: land the caller first, flip the entry after. Refs tracebloc/backend#1415 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
saadqbal
approved these changes
Aug 3, 2026
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
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes part of
tracebloc/backend#1415. Evidence:tracebloc/backend#1408.What this adds
repo-inventory.ymlat the repo root — the single source of truth for which reusables each of the 20 active repos must call, plus its visibility and release-train membership..github/workflows/caller-drift.yml— a report-only drift guard modelled onmerge-settings-drift.yml, which is the fail-closed pattern the ticket names.scripts/caller-drift.py— the comparison, andscripts/tests/caller-drift-selftest.py— 42 offline assertions that the guard's failure paths actually fail. The audit jobneeds:the self-test.The schema, and why absence fails
Every repo carries an entry for every reusable and every copy. Each entry is exactly one of:
required.github)exempt: "<reason>"divergent: "<reason>"A missing key is a guard failure, not a default. So is an
exemptwhose reason is empty, null, whitespace, or absent; a bareexemptscalar with no reason; a repo in the org but not in the file; a repo in the file but not in the org; an unknown top-level key; aschema_versionthe guard was not written for; anaudit_branchother thandevelop-first; an emptyreusableslist. Schema validation runs before any network call. There is no way to make the guard quiet by leaving something out — only by writing down why.The inverse is also checked: a caller that exists where the inventory says
exemptis reported as a stale exemption, and adivergentcopy that has come back into line is reported so it can be promoted torequired. Exemptions cannot quietly outlive their reason.Matching is on
uses:content, never on filenameThe ticket's correction is right that a filename audit cannot identify a caller, though the specifics differ from what it says — see What the ticket got wrong below. Each workflow is parsed as YAML and its resolved
uses:values are read. That also meanscode-quality.yml:60, a commented-out exampleuses:line, is not counted as a caller — a grep-based audit reports acode-qualitycaller in.githubthat does not exist.Measured counts — develop-first, 2026-08-03
Taken via the API on
developwhere that branch exists, else the default branch. Twelve of twenty repos default tomain/master, so a default-branch enumeration under-reports anything in flight.110 caller references across 17 repos, all pinned
@main. Three repos have no.github/workflowsdirectory at all:devex-bootstrap,e2e-test-agent,rfcs..githubaveraging-servicebackendclaude-skillscliclientclient-runtimedata-ingestorsdesign-systemdevex-bootstrapdocse2e-test-agentfrontend-appmodel-zoorelease-trainrfcsstart-trainingtracebloc-enginetracebloc-py-packagetracebloc-website✓present ·✗absent (exemption written) ·–structurally not applicable (private repo, so nopublic-pii-gate)Per reusable:
set-pr-status17 ·kanban-closure-router17 ·advance-deploy-env15 ·customer-priority-bump15 ·fr-pass-comment15 ·fr-gate14 ·code-quality11 ·public-pii-gate6 ·wip-limit-check0.Filename conventions, measured:
code-qualitycode-quality-caller.yml×11fr-gatefr-gate-caller.yml×14fr-pass-commentfr-pass-comment-caller.yml×15public-pii-gatepublic-pii-gate-caller.yml×6customer-priority-bumpcustomer-priority-bump.yml×15advance-deploy-envadvance-deploy-env-caller.yml×1 (.github),advance-deploy-env.yml×14set-pr-statusset-pr-status-caller.yml×1 (.github),set-pr-status.yml×16kanban-closure-routerkanban-closure-caller.yml×1 (.github),kanban-closure-routing.yml×16 — matching neither patternExemptions written — these are the findings
Today's state genuinely violates what the inventory should say, so current reality is encoded with an explicit reason on every gap. 220 entries in total: 140
required, 77exempt, 3divergent. The 80 non-requiredentries, grouped (19 + 20 + 31 + 10):Structural — defensible, not gaps (19)
public-pii-gateexempt on all 13 private repos: the gate scans PR text for PII that would be world-readable, and there is no public exposure to guard.fr-gateexempt on the 5 repos with nostagingbranch (.github,docs,devex-bootstrap,e2e-test-agent,rfcs) plusrelease-train, which drives the train and is exempt from riding it per RFC-BACKEND-1405 D1. No staging branch means no staging→prod hop to gate.wip-limit-checkexempt in all 20 repos (20) — it has zero callers anywhere in the org. Its own header says to delete the reusable once nothing references it. Requiring it anywhere would land this guard red on day one; wiring it up or deleting it is follow-up work.Unremediated caller gaps (31) — each says UNREMEDIATED in so many words:
code-quality×9 —.github,claude-skills,devex-bootstrap,docs,e2e-test-agent,model-zoo,release-train,rfcs,start-training. Matches the ticket exactly.fr-pass-comment×5 —claude-skills,devex-bootstrap,e2e-test-agent,release-train,rfcs. Cards there can only be advanced by hand.customer-priority-bump×5 —.github,devex-bootstrap,e2e-test-agent,release-train,rfcs.advance-deploy-env×5 —claude-skills,devex-bootstrap,docs,e2e-test-agent,rfcs. Merges there never move a card toOn dev.kanban-closure-router×3 andset-pr-status×3 —devex-bootstrap,e2e-test-agent,rfcs.public-pii-gateon public.github×1 — the only public repo without it. Sequenced behindbackend#1409, since the reusable is currently unarmed org-wide (PII_DENYLISTunset).Copy exemptions and divergences (10)
add-to-kanban.ymlandstale-backlog.ymlexempt indevex-bootstrap,e2e-test-agent,rfcs(no workflows directory) — their issues and PRs never reach the board.stale-backlog.ymlexempt inrelease-train— a deliberate omission: it is column-blind and would auto-close live pipeline work (backend#1408P3).cli/add-to-kanban.ymldivergent — pinsactions/add-to-project@v2.0.0againstv1.0.2everywhere else.cli/stale-backlog.ymldivergent — pinsactions/stale@v11againstv9everywhere else.release-train/add-to-kanban.ymldivergent — adds a wanted explanatory header, and also pinsadd-to-project@v2.0.0.Recording those three as
divergentrather thanrequiredmeans the guard reports the drift it already found instead of hiding it behind a green run. Realigning the pins is follow-up work.Fail-closed: every error path walked
Bugbot's recurring find in this epic is a failure path that reports success, or a precondition that fails open. For a drift guard that is the whole product, so every path was walked and is asserted by the self-test:
orglisting fails, or returns zero active reposrelease-train/repos.ymlunreadable, or has an emptyrepos:listtreearrayexit_codeis empty*branch fails the runA partially-read repo is never evaluated: one unreadable file marks the whole repo unknown. The final workflow step runs
if: always()and fails on anything other than a literal0, including an absent value. It never posts an all-clear comment, only failures, and only on scheduled or manual runs.Verified locally
actionlint— clean.ruff check scripts/— clean.scripts/tests/caller-drift-selftest.py— 42/42 pass, offline.required, a stale exemption, a copy content drift, a visibility mismatch, a release-train mismatch, an untracked repo, a ghost repo, and a caller for an unlisted reusable each exit 1.Deferred
The sync job that opens PRs for missing callers is deliberately not in this PR. Like
merge-settings-drift.yml, this guard is report-only and never mutates a repo, so the token needs org-wide read rather than standing write over every repo. Follow-up work, in rough order:wip-limit-check: wire it up, or delete the reusable.add-to-project@v1.0.2/stale@v9.public-pii-gateto.github, oncebackend#1409arms the reusable.devex-bootstrap,e2e-test-agentandrfcs, then flip their exemptions torequired.gate / gate), never filenames.What the ticket got wrong
The measurement contradicts the ticket's own correction pass in five places. Conclusions all survive; the numbers do not.
@main, so that part holds.devex-bootstrap,e2e-test-agent,release-train,rfcs" → three.release-trainhas three callers (advance-deploy-env.yml,kanban-closure-routing.yml,set-pr-status.yml).claude-skills— a repo the ticket does not mention at all — has four.fr-pass-comment×7" → ×5.code-quality×9,fr-gate×6 andpublic-pii-gateon public.githubare all confirmed exactly.code-qualityas one of the inconsistently-named reusables; it iscode-quality-caller.ymlin all 11 repos. And foradvance-deploy-env,set-pr-statusandkanban-closure-router, the only repo using the-caller.ymlform is.githubitself — so the original ticket's ".githubuses*-caller.yml, the others name the caller after the reusable" is closer to the truth than the correction that replaced it, except thatfr-gate,fr-pass-comment,public-pii-gateandcode-qualityuse-caller.ymleverywhere. The operative conclusion — match onuses:, not filename — is unaffected and still correct.wip-limit-checkis listed as one of the reusables with callers under two filenames. It has no callers at all. That is a finding the ticket missed entirely: a reusable nothing references.Two smaller corrections, both to
backend#1408:clipinsactions/stale@v11, notv10. Theadd-to-project@v2.0.0drift is confirmed, andrelease-trainhas the samev2.0.0drift — a second repo #1408 does not mention.model-zooandstart-traininghave fr-gate but no fr-pass caller" — both havefr-pass-comment-caller.ymltoday.🤖 Generated with Claude Code