release-train: develop -> staging - #268
Conversation
…8) (#264) * feat(conformance): assert WHO may skip a required review (backend#1978) `min_reviews` asserted HOW MANY approving reviews a branch requires. Nothing asserted WHO may merge without them: `required_pull_request_reviews. bypass_pull_request_allowances` appeared in neither the guard nor the inventory (measured: 0 occurrences in both). So `min_reviews: 1` was satisfied while an actor skipped it entirely, and a second app or a user added tomorrow read identically to today. The ruleset layer already treats its bypass list as an EXACT allowlist, and for this exact reason -- backend#1681 found `release-python` holding `always` tag bypass on three repos, two of which publish on the tag. Classic protection had the same hole and no check. Measured 2026-08-15 across all 16 train repos x 3 branch roles: develop no bypass actor anywhere staging + prod exactly App:tracebloc-release-train, everywhere That is the release train merging promotion PRs via its review-bypass (RFC-BACKEND-0008 D9/D14); it never bypasses `fr-gate`, which stays required. So the policy states the measured status quo and lands green. EXACT, not a floor: an unexpected actor is a finding, AND a MISSING one is too -- if the train loses its bypass the fleet cannot promote, and that should be a red check rather than a mystery on the next hop. Actors are normalised to one namespaced vocabulary (`login`, `Team:slug`, `App:slug`) so a user, a team and an app cannot collide on a bare name. DRAFT until the end-to-end mutation lands. The first attempt did not prove the assertion -- it crashed the guard with a NameError, because the finding strings referenced `read.branch` where only `branch` is in scope. Fixed; that the guard crashed LOUDLY ("this is not a drift report and not an all-clear") rather than reporting green is the fail-closed contract working. Re-run in flight. Selftest 176 pass / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(conformance): the prod bypass allowlist assumed every prod branch is train-promoted (backend#1978) The new `bypass_reviews` assertion produced six findings, and all six were the policy over-reaching rather than the fleet drifting. 14 of 16 train repos match the baseline exactly. The `prod` role encodes "the release train merges promotion PRs on this branch", which is why it grants `App:tracebloc-release-train` a review bypass. Three repos in the inventory are not on the train -- `release-train`, `rfcs`, `claude-skills` -- so the train promotes nothing into them and holds no allowance there. The file already draws exactly this distinction for `enforce_admins` via `not_a_pipeline_prod_branch`; `bypass_reviews` was added to OVERRIDABLE but no repo was given the override. `docs` is the other case: `required_pull_request_reviews` is null on staging and main (measured 2026-08-15), so there is no required review to bypass and an allowance would assert a permission that cannot be exercised. `release-train/main` now states its real allowlist -- ['LukasWodka', 'saadqbal']. That is the widest review bypass in the fleet and nothing asserted it before, which is the gap #1978 exists to close: a third name there is now a finding. Both shared reasons are extended to name the key they now cover. `docs_no_required_review` claimed the divergence "names `min_reviews` alone", which a second key beside it would have falsified -- the same defect this file already records happening once with `required_checks`. Verified: caller-drift-selftest 176 pass / 0 fail; full fleet audit reports zero drift findings and `protection: OK` on all 19 repos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…s promotion branches (backend#1975) (#265) frontend-app was the one train repo where a failing test could not block a promotion to prod. Both `staging` and `main` required `gate / gate` plus the two `quality/*` content gates and NO functional suite at all, while every peer already required one (backend: `Django + Bandit plugin`, client-runtime: `pytest`, design-system + tracebloc-website: their unit suites). It is the customer-facing UI. `Unit tests (Vitest)` is now armed on both branches, and this states it, so a later removal is drift rather than silence. Armed while green, and verified reporting first: - tests.yml triggers on `pull_request: branches: [develop, staging, main]` on all three branches, and the job named `Unit tests (Vitest)` was observed reporting on a `main`-targeting PR. A required context that can never report leaves every PR waiting forever (client#665, pii-gate/pii-check). - Green in the last 14 runs of tests.yml measured PER JOB. The workflow conclusion mixes six jobs and says nothing about this one. Not armed, deliberately: `API contract (real dev-API)` depends on a live dev API, so requiring it would block prod promotions on someone else's uptime; `E2E tests (Cypress)` is the same bet with a flakier suite. `divergent` is the only cell shape that can state a repo's own set. Here it diverges by requiring MORE than the fleet baseline, not less; the comparison in evaluate_protection() is a subset test, so this strictly strengthens the assertion. Verification: caller-drift-selftest.py 176/176 pass; the live audit stays green; and the new cell was mutation-proved through the real evaluate_protection() against the live branches - renaming the armed context reddens both `staging` and `prod`, with the anchor asserted as applied. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…266) This repo has eight selftest suites. Before this file, SEVEN ran only from their own path-filtered workflow and NOT ONE was a required status check. A change to caller-drift.py ran the caller-drift suite and nothing else; a red suite blocked no merge at all. That is this org's own rule turned on itself: a guard in a non-required job is advice, not a gate (backend#1729). These suites guard the reusables every other repo's CI depends on -- the last place advice is good enough. NO `paths:` FILTER, deliberately, for two reasons: 1. The suites are cross-cutting. caller-drift-selftest imports the guard, kanban-deploy-state-selftest extracts and runs shell out of TWO workflows, and selftests-cover reads the Makefile. A filter naming the obvious file misses the edit that actually broke them. 2. A REQUIRED check that is path-filtered never reports on a PR the filter skips, so the PR waits at "Expected - waiting for status" forever. This org has hit that twice (client#665, pii-gate/pii-check). The whole suite is 34 seconds and 346 assertions, so filtering buys nothing. It calls `make selftests` -- the same target a contributor runs and the same one the pre-push hook runs -- so this job cannot drift from local (backend#1606). That target depends on `selftests-cover`, which fails when a selftest file exists that no target runs, so adding a suite without wiring it is caught here. Action pins are copied from this repo's own caller-drift.yml rather than written from memory. NOT required yet, on purpose: a required check that has never reported bricks every open PR. Merge this, let it report green on develop, then add `selftests` to branch protection. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
…2036, backend#2037) (#270) PROJECTS_KANBAN_TOKEN is ONE user PAT, shared by fifteen workflows, both crons, the conformance gate, and the shell of the person who owns it. It was measured exhausted in practice -- client-runtime advance-deploy-env run 31776053792: `API rate limit already exceeded for user ID 54042461`, exit 1, card stranded. The `tracebloc-release-train` App now carries `organization_projects: write`, so board writes can authenticate as an installation whose budget is separate from any person's interactive usage and does not disappear at offboarding. TRANCHE 1 -- the three workflows that touch ProjectsV2 and nothing else: kanban-archive archiveProjectV2Item kanban-columns reads the Status field set-pr-status updateProjectV2ItemFieldValue `advance-deploy-env` is deliberately NOT here. It also needs no extra permission, but it is the workflow whose failure motivated this ticket and it writes on every push to every branch -- it gets its own tranche and its own hop to prove it. `owner:` on the mint step makes the installation token ORG-scoped. Without it the token is scoped to the calling repository, which cannot touch an org ProjectV2 -- this is the failure that reads as a wiring bug and is actually a scope bug. NO FALLBACK TO THE PAT. A `token || secrets.PROJECTS_KANBAN_TOKEN` fallback would let a broken App path keep working silently, so the migration would look done while nothing had migrated. That is the exact defect class backend#1680 exists to remove, and it would be self-defeating to introduce one here. If the mint fails, the job fails. In kanban-archive, GH_TOKEN moves from job-level env to per-step, because a job-level env cannot read a step output. That is an improvement rather than a workaround: the file now names which steps hold a credential. ALSO FIXES backend#2037. set-pr-status polled five times for the card, logged "not on project after 5 retries -- skipping", and exited 0. A PR that lost the add-to-kanban race therefore sat at No Status behind a GREEN check, so the board's "no card sits at No Status" invariant was broken by the workflow that reports it upheld -- backend#1950 lost by four seconds and nobody saw it. Now ::error:: and exit 1. The retry budget stays 5 x 5s on purpose. Widening it in the same change would hide how often the race is actually lost, which is the thing failing closed is meant to reveal. Callers need no change: every one uses `secrets: inherit`, and both new org secrets are set at `visibility: all` in the Actions and Dependabot scopes. PROJECTS_KANBAN_TOKEN is untouched and still live for the other twelve workflows. It is deleted in a final cleanup tranche, after a full hop has exercised the App path -- so rollback here is a revert, not a secret restore. Verified: actionlint clean; all three parse; no GH_TOKEN in these three files resolves to the PAT any more. Refs backend#2036 Closes backend#2037 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#269) Layer 2 of backend#1563 -- the automated post-release bump-PR opener -- shipped and was never wired up. Measured today: ZERO `uses:` references anywhere in the org, and it cannot be called as written because it requires a `pr-token` secret no repo supplies. Nineteen repos carried an exemption row for it. Deleting rather than wiring, for three reasons: 1. Layer 1 (`version-bump-gate`) is armed and required, and it already converts the failure mode from a silent stall into a loud block. That is what backend#1561 actually needed -- the pain there was that nobody KNEW the version was stale, not that bumping it by hand was expensive. 2. Wiring it means provisioning a second fleet-wide credential in the same week backend#2036 consolidates board writes off a shared user PAT and onto the `tracebloc-release-train` App. The version worth having after #2036 would authenticate as the App, so it is a different workflow anyway. 3. 402 lines of never-executed workflow plus 19 exemption rows is contract a human reads and skips. Deleting shrinks what the inventory has to say. Removed: the workflow, its `reusables` entry, all 19 exemption rows, and the `version_bump_pr_never_wired` shared reason. Kept, marked as history: the narrative in `check_source_reusables` and its selftest that cites this file as the reusable which exposed the enumerate-the-inventory-not-the-source blind spot (backend#1681). The lesson is why that function exists; both notes now say the file was deleted, so a reader grepping for it is not left hunting. `blocked_gate_rollout_pending` compared itself to this anchor and now names only `wip_limit_check_has_no_callers`, plus a line that deletion is the other way an unwired reusable can end. Verified: caller-drift-selftest 176 pass / 0 fail. Closes #1563 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ackend#2036) (#271) The workflow this ticket was filed about. On 2026-08-14 06:22 UTC, run 31776053792 on client-runtime died with gh: API rate limit already exceeded for user ID 54042461 exit 1 -- because fifteen workflows, both crons, the conformance gate and one person's own `gh` all spend the same user PAT's 5,000/hr. It failed CLOSED, which is correct, and the cost was still a card sitting behind its own shipped code until a human noticed. Its own tranche, on purpose. It needs no permission tranche 1 did not already have, but it writes on every push to every tracked branch in sixteen repos, and it is the single workflow that keeps the board honest about what has shipped. If one thing in this migration deserves its own hop to prove it, this is it. `owner:` makes the installation token ORG-scoped -- a repo-scoped token cannot write an org ProjectV2, and that failure reads like a wiring bug rather than the scope bug it is. The mint step carries the SAME `if:` as the step that consumes it, so a push to an untracked branch, or one whose commits carry no PR, mints nothing rather than spending an API call to discover it had no work. NO FALLBACK TO THE PAT, consistent with tranche 1: a fallback would let a broken App path keep working silently, so the migration would report success while nothing had migrated -- the defect class backend#1680 exists to remove. Blast radius: callers pin `@main` and this repo works on `develop`, so merging this changes nothing in the fleet until .github is promoted. PROJECTS_KANBAN_TOKEN stays live for the remaining eleven workflows and is deleted in a final cleanup tranche, after a hop has exercised the App path. Verified: actionlint clean; parses; no GH_TOKEN in this file resolves to the PAT. Refs backend#2036 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…esent (backend#1680) (#272) selftests.yml was added to be a required status check -- its own header says so, and cites the org rule it turns on itself: a guard in a non-required job is advice, not a gate (backend#1729). Nothing made it required. So the eight suites guarding the reusables every other repo's CI depends on could go red and block no merge, and the protection audit reported conformance without ever looking. `selftests` is now armed on develop and staging, and this states it, so a later removal is drift rather than silence. Armed while green, and verified reporting first: - selftests.yml triggers on `pull_request:` with NO branches and NO paths filter, deliberately (its header explains why), so it reports on every PR. A required context that can never report leaves the PR waiting forever (client#665, `pii-gate/pii-check`). - Green in all 4 runs since it landed, measured per job, including on the release-train/to-staging head of promotion PR .github#268. NOT armed on prod, and that is sequencing rather than a decision against it: `selftests.yml` reaches staging only when the promotion carrying it merges, so until then a staging -> main PR head cannot report the context. The comment on the cell carries the follow-up. `divergent` is the only cell shape that can state a repo's own set. It diverges by requiring MORE than the fleet baseline; the comparison in evaluate_protection() is a subset test, so this strictly strengthens the assertion and the live audit could not have gone red from arming alone. Verification: `make selftests` green; the new cell mutation-proved through the REAL evaluate_protection() against the LIVE branches -- renaming the armed context reddens both develop and staging, with the anchor asserted as applied (2 occurrences), and the unmutated cell probes clean. Found by Bugbot on release-train promotion PR .github#268. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…che 3 (backend#2036) (#273) Four workflows, seven token sites: kanban-closure-router 3 card + sibling-merge label + closure comment fr-pass-comment 2 card + outcome comment + reaction kanban-reconcile 1 job-level env, expanded to 6 per-step customer-priority-bump 1 priority label on the issue All four need `issues: write` as well as `organization_projects: write`, which the App gained after tranche 2 -- that is why they are here rather than in tranche 1. `add-to-kanban` is deliberately NOT here. It is a COPY, duplicated byte-for-byte into all 19 repos with caller-drift comparing blob shas against .github's copy, so migrating it is a 19-PR sweep where every other repo must merge BEFORE .github or the org audit goes red on main until the last one lands. It gets its own tranche for that reason alone. In kanban-reconcile, GH_TOKEN moves from job-level env to the six steps that actually call gh, because a job-level env cannot read a step output. Three of its nine steps never needed a credential and no longer carry one. THREE STALE PROSE REFERENCES FIXED IN THE SAME CHANGE, because leaving them is the defect this epic is about -- a document that describes behaviour the code no longer has: - The MIN_SCOPE guard's ERROR MESSAGE told operators to "Check PROJECTS_KANBAN_TOKEN still has org-wide contents:read". That is remediation for a credential this workflow no longer uses. It now names the App, and names the new failure mode: an App is installed on SELECTED repos, so a repo added to the org but never added to the installation reads as absent exactly the way an exhausted PAT did. - "the PROJECTS_KANBAN_TOKEN write rule" is now "the board-write rule". A rule named after a secret goes stale the moment the secret does. - The counted-not-fatal rationale cited the shared-PAT 5,000/hr budget as the likeliest cause of a 403. The App removes that cause but not the class, so the comment now says which part changed and which part did not. NO FALLBACK TO THE PAT, consistent with tranches 1 and 2. Still on PROJECTS_KANBAN_TOKEN after this: add-to-kanban (the copy sweep), fr-gate (its own tranche, last -- it is a required check on every promotion, so if its board reads break, the train that would carry the fix is the broken thing), and the four audits caller-drift / standards-sync / merge-settings-drift / bricked-prs. Verified: actionlint clean on all four; all four parse; kanban-columns selftest 10/10; caller-drift selftest 176/176; no GH_TOKEN in these files resolves to the PAT. Refs backend#2036 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4d70407. Configure here.
| # not the credential: backend#2036 moved these writes to the App, and a rule | ||
| # named after a secret goes stale the moment the secret does. | ||
| env: | ||
| GH_TOKEN: ${{ steps.app-token.outputs.token }} |
There was a problem hiding this comment.
App loop still aborts on write
Medium Severity
The North Stars shield still issues bare gh api label writes inside its loop after switching GH_TOKEN to the App installation token. Under set -euo pipefail, the first 403 (likely a repo outside the App installation) aborts the whole sweep, so later North Stars issues never get keep-open. The membership step in this same migration already treats that failure class as counted-not-fatal.
Triggered by learned rule: Bare API writes inside loops must not abort the sweep under pipefail
Reviewed by Cursor Bugbot for commit 4d70407. Configure here.


Automated promotion by the release train (RFC-0008 D14). Head is the train-managed
release-train/to-stagingbranch (a mirror ofdevelop), so it never collides with a human PR. Merged only when the fr-gate is green.Note
Medium Risk
Changes how all kanban automation authenticates and what blocks merges (new required selftests/Vitest contexts); misconfigured App secrets or installation scope would fail workflows closed until fixed, which is intended but operationally sensitive.
Overview
Kanban / GitHub API auth (backend#2036) — Reusable and cron workflows that read or write org ProjectV2 (
advance-deploy-env,set-pr-status,fr-pass-comment,kanban-closure-router,kanban-reconcile,kanban-archive,kanban-columns,customer-priority-bump) now mint an org-scopedtracebloc-release-traininstallation token viaactions/create-github-app-tokenand use it asGH_TOKEN, with no fallback toPROJECTS_KANBAN_TOKEN.kanban-closure-routeralso supplies that token on the “Determine target Status” step so the cross-repo closer GraphQL lookup can run. Reconcile/archive jobs moveGH_TOKENfrom job-level env to per-step env where needed.Fail-closed behavior —
set-pr-statusexits 1 (with::error::) if the PR is still not on the project after retries, instead of skipping with success.New required CI — Adds
selftests.ymlto runmake selftestson every PR (no path filter) and wiresselftestsas an extra required check on.githubdevelop and staging inrepo-inventory.yml.Fleet inventory / guard — Documents
bypass_reviewson develop/staging/prod (includingApp:tracebloc-release-trainon promotion branches);caller-drift.pyaudits bypass allowlists from the API. frontend-app staging/prod now require Unit tests (Vitest) in addition to the baseline gates. Removesversion-bump-pr.ymlfrom reusables and deletes the workflow file; drops related inventory exemptions.Misc — Per-repo
bypass_reviewsoverrides for docs, release-train, and non-train repos; reconcile error text updated for App installation scope.Reviewed by Cursor Bugbot for commit 4d70407. Bugbot is set up for automated code reviews on this repo. Configure here.