Skip to content

release-train: develop -> staging - #268

Open
LukasWodka wants to merge 8 commits into
stagingfrom
release-train/to-staging
Open

release-train: develop -> staging#268
LukasWodka wants to merge 8 commits into
stagingfrom
release-train/to-staging

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed release-train/to-staging branch (a mirror of develop), 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-scoped tracebloc-release-train installation token via actions/create-github-app-token and use it as GH_TOKEN, with no fallback to PROJECTS_KANBAN_TOKEN. kanban-closure-router also supplies that token on the “Determine target Status” step so the cross-repo closer GraphQL lookup can run. Reconcile/archive jobs move GH_TOKEN from job-level env to per-step env where needed.

Fail-closed behaviorset-pr-status exits 1 (with ::error::) if the PR is still not on the project after retries, instead of skipping with success.

New required CI — Adds selftests.yml to run make selftests on every PR (no path filter) and wires selftests as an extra required check on .github develop and staging in repo-inventory.yml.

Fleet inventory / guard — Documents bypass_reviews on develop/staging/prod (including App:tracebloc-release-train on promotion branches); caller-drift.py audits bypass allowlists from the API. frontend-app staging/prod now require Unit tests (Vitest) in addition to the baseline gates. Removes version-bump-pr.yml from reusables and deletes the workflow file; drops related inventory exemptions.

Misc — Per-repo bypass_reviews overrides 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.

LukasWodka and others added 3 commits August 15, 2026 10:50
…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>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread .github/workflows/selftests.yml
@tracebloc-release-train tracebloc-release-train Bot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 15, 2026
LukasWodka and others added 3 commits August 15, 2026 12:56
…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>
LukasWodka and others added 2 commits August 15, 2026 13:28
…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>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@tracebloc-release-train tracebloc-release-train Bot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 15, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-nudge Toggled by the release train to (re-)fire the fr-gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant