Skip to content

release-train: develop -> staging - #365

Merged
tracebloc-release-train[bot] merged 4 commits into
stagingfrom
release-train/to-staging
Aug 27, 2026
Merged

release-train: develop -> staging#365
tracebloc-release-train[bot] merged 4 commits into
stagingfrom
release-train/to-staging

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 27, 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 org-wide GitHub Actions behavior (kanban project writes, new status API sweeps, and closing-ref policy) with broad test coverage but real fleet impact if routing or tokens misbehave.

Overview
This promotion bundles several fleet CI / kanban changes in tracebloc/.github.

Kanban model (backend#2722): Completed issues are routed to Done instead of mirroring a closing PR’s deploy column. That removes the GraphQL “closer” logic in kanban-closure-router.yml, stops advance-deploy-env from advancing closing issues cross-repo, and aligns kanban-reconcile so PR/commit-closed issues also terminalize to Done rather than deriving On dev / staging columns. App token comments drop issue-read scope where it’s unused; selftests are updated for the slimmer branch_status_map usage.

Merge-conflict visibility (backend#2637): New scheduled conflict-gate workflow plus scripts/conflict-gate.py sweep open PRs and post a conflict-gate / mergeable commit status (failure/success/pending) because conflicted PRs run nopull_request checks. Includes selftests and mutation coverage in the Makefile.

Closing-ref gate (backend#2616, #2556):closing-ref-gate.py now requires title-to-reference agreement: Closesor canon-derived non-closing forms like Part of parsed from org-standards.md (with fail-closed derivation). Callers listen for PR edited; set-pr-status jobs only run while the PR is open so merged PR title/body edits don’t demote cards. Non-draft board status label becomes Code review. Docs (CLAUDE.md, org-standards.md) match the new rules.

Reviewed by Cursor Bugbot for commit 33944d5. Bugbot is set up for automated code reviews on this repo. Configure here.

LukasWodkaand others added 4 commits August 27, 2026 17:54
…e truthful (backend#2556) (#360)
* fix(closing-ref): re-run when the title changes, and let a child PR be truthful (backend#2556, backend#2616)
Two defects in one gate, fixed together because either alone leaves the other's
remedy unusable.
BACKEND#2556 -- THE GATE READS THE TITLE AND NEVER RE-RUNS WHEN IT CHANGES.
`closing-ref` decides its verdict from the PR title and body; the callers
triggered on `[opened, reopened, ready_for_review, converted_to_draft]`, and
`edited` is the only event GitHub fires when either field changes. So the two
inputs the gate reads were the two inputs that could change without re-running
it. That is a bypass, not a gap: open a PR titled `chore: tidy up`, the gate
records NOTHING_NAMED and goes green, then retitle it to `fix(1234): ...` with
nothing linked -- no event fires, the green stands. It also made the remediation
unusable in the other direction: 20 sync PRs had to be cleared with 20 manual
`gh run rerun` calls (backend#2555). Same one-word fix, same reason, as
`fr-gate-caller.yml` (backend#1945).
A CONSEQUENCE THAT HAD TO BE GUARDED, not discovered later. Every trigger this
workflow had was reachable only on an OPEN PR, so `set-status` writing Status
unconditionally was always correct. `edited` is not: GitHub fires it when a
MERGED PR's title or body is edited, and this is the one board writer in the
fleet that does not compare pipeline rank. Without the new guard, fixing a typo
in a shipped PR's description would drag its card from `Prod` back to
`Code review`. Both jobs are now `if: ...state == 'open'`.
BACKEND#2616 -- ONE SATISFYING FORM, AND A CHILD PR CANNOT TRUTHFULLY USE IT.
The checker read `closingIssuesReferences` and nothing else, and that field is
populated only by a closing keyword. So a PR whose title named a ticket had
exactly one satisfying form: promise to close it. For this org's most common
shape -- a parent ticket with several child PRs titled `type(NNNN): ...`, none
of which finishes it -- the only remedies were a FALSE `Closes`, or deleting the
number from the title, which passes the check by removing the traceability the
check exists to enforce. Four PRs in one day took the second (.github#349, #350,
#352, #353) and #354 wrote that workaround into the canon.
A declared NON-CLOSING body reference now satisfies the title, reported as its
own state (`MENTIONED`) so a green run still says which promise was made. The
closing link stays the stronger form, and `WRONG_REPO` is decided BEFORE any
body reference is consulted -- a truthful `Part of tracebloc/backend#304` must
not mask a `Closes#304` that closes `.github#304` on merge.
DERIVED, NOT RESTATED (rule 1). The non-closing vocabulary is PARSED out of
`org-standards.md`, which already declares the org's partial-work form. Add
`Refs <owner>/<repo>#N` to the canon and the gate accepts it with no code
change. The derivation fails closed in both directions: an unreadable canon and
a canon declaring no non-closing form are separate cannot-tells (exit 2), never
a silent reversion to closing-only -- reverting is the defect itself. GitHub's
closing keywords are the one list here that cannot be derived from this org, so
they are named, cited, and used only to subtract.
The canon and CLAUDE.md carried two statements this change makes false -- that
the check fails unless the body carries `Closes`, and to keep the number out of
the subject. Both are rewritten; classify() reports IN_SYNC.
EVIDENCE
closing-ref-gate-selftest 156 assertions, all passed (was 109)
closing-ref-gate-mutations 53 mutations, 0 stale, 0 uncaught (was 36)
ruff / actionlint / shellcheck / house-rules clean
standards-sync selftest 60 checks, 0 failed
The mutation harness now rewrites the two WORKFLOW files as well as the checker:
`edited` and the open-state guards are guarantees declared in YAML, and rule 5
does not exempt a guard for living in a different language.
Part of tracebloc/backend#2556
Part of tracebloc/backend#2616
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(closing-ref): an escaped Unreadable reddens a case instead of killing the suite
The multi-word-keyword mutation was scored UNCAUGHT for the right reason: it made
`evaluate` raise, which killed the suite mid-run, and the harness refuses to
count a broken harness as coverage. `evaluate` grew a path that can raise for a
NEW reason (the derivation from org-standards.md coming back empty), so the
positive cases now go through a wrapper, exactly as `value` already does for
every other call.
Part of tracebloc/backend#2616
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(closing-ref): the derived-vocabulary cases report a refusal instead of crashing
`value` returns the exception TEXT on a refusal, and a string is iterable -- so a
case that walked the derived list iterated characters and raised IndexError on the
space in "RAISED Unreadable: ...". Normalised once. Found by the multi-word-keyword
mutation, which is the job: it was scored UNCAUGHT for breaking the harness rather
than being caught by a case.
Part of tracebloc/backend#2616
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…nd#2722) (#361)
* fix(kanban): a completed issue is terminal, not a deploy state (backend#2722)
Closed issues were landing in deploy columns because two mechanisms deliberately
put them there, not because either was failing.
1. kanban-closure-router.yml mirrored the closing PR's Status onto the issue, so
a PR-closed issue inherited On dev / FR on staging / Prod. Its own declared
contract said so.
2. advance-deploy-env.yml then marched those cards onward (backend#1600), because
issues parked at On dev never advanced when the code shipped and drifted
permanently -- all 20 drifted cards on 2026-08-06 were closed issues, 0 PRs.
#1600 was right that a closed issue must not be stranded and wrong about where to
put it. `Done` is terminal, so nothing can drift, and kanban-archive sweeps it off
the board daily -- which answers #1600 more completely than advancing did.
So: the router routes a completed issue to Done regardless of how it was closed,
and the closing-issue block in advance-deploy-env is removed. Both halves, because
fixing only one looks fixed while the other drags the cards straight back in.
The closer lookup goes too. It existed only to tell a hand-close from a PR-close
so they could route differently; with one destination there is nothing to tell
apart. It was also the source of two fail-wrong defects (.github#126, #127), and a
call never made cannot fail. CLOSER_TYPE / CLOSING_PR_BASE were read nowhere else.
`permission-issues` drops from advance-deploy-env with the only reads that needed it.
Checked rather than assumed: fr-gate ranks Done 11 against On dev 6, so terminal
cards satisfy both gates instead of blocking promotions.
Cost of the status quo, measured: 117 closed issues cleared out of deploy columns
by hand in one session (18 + 55 from FR on staging, 44 from On dev). Two of those
passes were functional-review batches, so the next prod payload would have read 73
items larger than the work it contained.
Guarded now, because it was not: flipping the router's destination left all 29
selftest suites green. kanban-deploy-state-selftest asserts both halves, reading
the completed arm out of the workflow rather than restating it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(kanban): the weekly backstop must terminalise too (backend#2722)
Bugbot, High. The router now sends every completed issue to Done, but
kanban-reconcile.yml still derived a deploy stage from the closer`s base branch
and wrote On dev / FR on staging / Prod for exactly those cards. Fixing one
writer and not the other is worse than fixing neither: the weekly sweep would
have put every card back, the slower job silently undoing the faster one --
which is the failure .github#295`s own comment was written to prevent, arriving
from the other direction.
A deploy column is a property of a PR. A finished ISSUE belongs in Done whether
or not its fix shipped, and issue cards parked in deploy columns are what stops
kanban-archive clearing them.
The closer lookup stays, because having one still distinguishes the two cases in
the LOG -- but both arms now reach the same column, which is the rule.
Second finding, same shape one layer out: the selftest read only the router and
advance-deploy-env, so a green run confirmed the two EDITED files rather than
the invariant. It now extracts the reconcile`s closer arm too and asserts it
writes only DONE_OPT, names no deploy column, and reaches no branch->stage
mapper. Mutation-proved: restoring the On dev write reddens it.
Also removes two now-dead option ids shellcheck flagged, and the comment that
justified them, which my own change had made false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(kanban): realign the mapper selftest with a terminal completed issue (backend#2722)
The selftest already encoded #2722 for the router -- one call site, not two --
but still required kanban-reconcile.yml to keep one. Removing reconcile`s
router-miss derivation made that false, so three assertions had to follow:
* reconcile`s mapper call sites: 1 -> 0. Its only one was the closed-completed
issue arm. The merged-PR lookback still places PR cards in deploy columns but
always mapped its branches INLINE, so the count did not move because of it.
* the DEST-arm invariant is inverted rather than deleted. It asserted every
Status with an option id needed an arm, or the backstop silently skipped the
repos an override exists for (.github#304) -- true while reconcile DERIVED a
column, meaningless now there is no $DEST. It now asserts there are no arms,
plus that the extractor can still find one in a fixture, so "no arms" cannot
be confused with "the reader broke".
* the stderr-preservation loop drops reconcile, which has no stderr to preserve.
Its absence is asserted above rather than left implicit.
All 15 selftests pass and actionlint reports 0 across every workflow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#362)
set-pr-status.yml's header said the non-draft card moves to "In review". That
option was renamed "In review" -> "Code review" in #20 (7dbaabf) and no longer
exists on the board -- verified against the live Status field, whose options are
Backlog, North Stars, Ready, In progress, Code review, On dev, Staging (agent
review), FR on staging, Ready for prod, Prod, Done, Cancelled.
The code has been correct throughout: the job writes `status_name=Code review`
(:108), and the same header already says "the SAME event that puts a card in
`Code review`" thirty lines further down. So the file disagreed with itself, and
the wrong half is the part a reader meets first.
Comment-only. No workflow logic, no behaviour change.
Swept the repo for the same phrasing; this was the only board reference. Two
other matches are deliberately left alone: repo-inventory.yml:635 uses "while
this PR was in review" to mean review as an activity, which is correct English
and not a column name, and standards-sync-selftest.py:428 is a Python `in`
operator, not the phrase.
make check: 95 passed 0 failed, 29 passed 0 failed, EXIT=0.
…d#2637) (#359)
* ci(conflict-gate): a conflicted PR reads red, not empty-green (backend#2637)
A PR with a merge conflict against its base dispatches NONE of its
`pull_request` workflows: GitHub cannot compute the merge ref those runs are
keyed on. Every drift and source-of-truth guard is therefore silently inactive
on exactly the PRs most likely to need it, and the rollup shows nothing red
because nothing ran.
Measured on model-zoo#206 while it was DIRTY against develop: 0 workflow runs
on its head sha, one rollup entry (Cursor Bugbot, SUCCESS), and all seven of
model-zoo/develop's required contexts absent. PRs #204 and #205 either side of
it got their full matrix.
Adds an org-wide sweep that writes a commit status onto each open PR's head sha
- failure when conflicted, success when clear, pending when GitHub will not say
- which is the only signal that can reach a PR with no merge ref.
Not required anywhere yet, deliberately: a 30-minute sweep would leave a
freshly-opened PR at "Expected - waiting" until the next run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(conflict-gate): a case that CRASHES is not a case that CAUGHT (backend#2637)
Two mutations -- drafts skipped in plan(), and an unreadable PR list returning
no error -- emptied a list the suite then indexed, so it died with an IndexError
instead of reporting a FAIL. The harness correctly scored both as 'broke the
harness' rather than caught: a traceback proves nothing about coverage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(conflict-gate): record the green-but-conflicted shape the watcher cannot see (backend#2637)
backend#2257, measured live and CONFLICTING, carries 8 workflow runs and all
eleven of backend/develop's required contexts present and SUCCESS -- computed
against a merge base that no longer exists. bricked-prs.py reasons from an
ABSENT required context, so with nothing missing it cannot see this PR at all.
That is the case that justifies asking about mergeability directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(conflict-gate): a status is written only when it would change something (backend#2637)
GitHub caps statuses at 1000 per sha AND context. A 30-minute sweep is 48
writes a day onto an unchanged head, so a PR left open three weeks would
exhaust the cap and every later write would 422 -- the gate going silent on
exactly the stalest PRs, which are the ones most likely to have conflicted.
The current state is read out of statusCheckRollup, which the PR list already
returns, so this costs no extra API call. A truncated rollup omits our context,
which reads as 'no status yet' and produces a write -- the safe direction.
The case fold is load-bearing: GraphQL reports SUCCESS, the Statuses API takes
success, and comparing unfolded would make every status look changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(conflict-gate): assert the workflow is ARMED, not merely present (backend#2637)
Everything the suite asserted proved the SCRIPT was right; none of it proved
anything RUNS it. Adds nine assertions parsing conflict-gate.yml as YAML (a
"run:" line under a comment or an "if: false" greps identically to a live one)
and six workflow mutations, so the harness now rewrites both files.
The regression most likely to be made in good faith has its own mutation:
someone asks why this does not run on PRs, adds a "pull_request:" trigger, and
the gate becomes inert on exactly the conflicted PRs it exists for while looking
more thorough. Rule 5 does not exempt a guarantee for being written in YAML.
The baseline guard now covers both targets: a mutation left in the workflow by a
killed run would become the next run's premise just as silently as one left in
the script.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(conflict-gate): read the current status via REST, not the rollup (Bugbot, #359)
Bugbot raised this as high on the previous head, and it was right.
"gh pr list --json statusCheckRollup" resolves "commit.status" underneath, and
GraphQL REFUSES that subfield on a PRIVATE repo unless the token also holds
"actions: read" -- measured under backend#2157 and documented in
bricked-prs.yml, which declares "permission-actions: read" for exactly this
reason. This gate's mint deliberately holds only pull-requests:read and
statuses:write, so the dedup added in c3b8279 would have made open_prs raise on
every private repo in the org: each becomes COULD NOT EVALUATE and the sweep
exits 2 having judged almost nothing.
The trap was documented in a file I read while writing this one, which is the
argument for measuring a narrow token rather than reasoning about it.
Reads "GET /repos/{o}/{r}/commits/{sha}/status" instead. It reads commit statuses
and nothing else, so the statuses permission already held covers it; it cannot be
refused for a scope this job has no other use for; and it has no pagination cap to
straddle. One GET per open PR, ~50 per sweep. The mint stays at two permissions.
Two new mutations pin it: switching back to the rollup, and treating an
unreadable current state as agreement (which would silently stop reporting
whenever the status read flakes).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

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

@cursorcursorBot 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 33944d5. Configure here.

Comment thread.github/workflows/set-pr-status-caller.yml
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 27, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Promoted with 1 open Medium/Low Bugbot finding(s), per the severity policy in release-train's README (High stops the line; Medium/Low are recorded and ship, at both hops):

Caller fires edited before callee guard

This is a second look at once-reviewed code -- it passed per-feature review on the source branch, and has NOT had functional review yet (that happens on staging). Fix forward on develop if any is real.

What the train did with each:

@tracebloc-release-train
tracebloc-release-trainBot merged commit 50c8f65 into stagingAug 27, 2026
42 checks passed
@tracebloc-release-train
tracebloc-release-trainBot deleted the release-train/to-staging branch August 27, 2026 19:39
Sign up for freeto 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.

1 participant

@LukasWodka