Skip to content

release-train: develop -> staging - #190

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

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

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 10, 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
Removing the public PII gate eliminates automated scanning of PR titles/bodies/commit messages on public repos; kanban and advance-deploy-env changes affect board state fleet-wide, though rename fallbacks limit breakage during the column transition.

Overview
This promotion batch updates org-wide GitHub automation in tracebloc/.github: kanban workflows, release hygiene, and shared standards.

Kanban column rename (backend#1592) — Staging merges and deploy advancement now target "Staging (human review)" instead of "FR on staging", with fallback resolution and matching rank() logic in advance-deploy-env, fr-gate, fr-pass-comment, kanban-closure-router, and kanban-reconcile so automation works on either side of a one-shot board rename.

advance-deploy-env — When promoting PRs, it also advances closing issues on the board (cross-repo), with fail-closed GraphQL handling (distinguish benign non-PR refs from real API failures), higher page limits, and guards when issue/project lists are truncated.

Public PII gate removed — Deletes public-pii-gate.yml, its selftest workflow, and scripts/tests/pii-gate-selftest.sh; trims related comments in code-quality callers. org-standards.md now states gitleaks covers secrets in code and PR prose on public repos is manual after retirement (backend#1409).

New version-bump-pr.yml — Reusable workflow (backend#1563 Layer 2) that opens a chore/version-bump PR when the declared version already shipped but publishable paths changed, using a PAT so CI runs on the PR; it does not tag or publish.

Also adds CLAUDE.md with synced org standards and minor comment/inventory tweaks (caller-drift, code-quality-caller).

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

LukasWodkaand others added 15 commits August 6, 2026 09:55
Managed sync from tracebloc/.github/org-standards.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…github#166)
The closing-issue advancement swallowed gh api failures into {} (a token/rate-limit/
permission error read as 'closes no issues' and silently skipped advancement, run
green) and capped closingIssuesReferences at 30 with no totalCount (a PR closing >30
silently truncated). Now: fail-closed (RUN_FAILED=1 + continue) on any lookup failure,
first:100 + totalCount surfacing on truncation, projectItems first:20.
…lCount (Bugbot .github#181)
The v1 fail-closed reddened every promotion carrying a PR_NUMBERS entry that isn't
actually a PR (issue ref / bogus number). Capture output and skip QUIETLY on 'Could
not resolve to a PullRequest'/Not Found, fail CLOSED only on a real infra failure.
Add the projectItems totalCount check for parity with the closingIssuesReferences fix.
Managed sync from tracebloc/.github/org-standards.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gbot .github#181)
'Not Found' is also what a scoped-down/expired token returns, so skipping it as
benign re-opened the fail-open. Match only 'Could not resolve to a PullRequest';
any access/transport failure now falls through to RUN_FAILED.
…ub#181)
Bare cis_out=$(...) under set -euo pipefail aborts the step on a non-zero substitution
(High) -- restore the 'if ! cis_out=$(...)' form the per-issue lookup already uses.
And printf|grep -q can return 141 via SIGPIPE under pipefail even on a match (Medium)
-- use a here-string so grep's own exit status is what's tested.
…(backend#1630)
Caught by Bugbot on the model-zoo staging mirror (model-zoo#131, held): the
same promotion that deletes public-pii-gate-caller.yml also carries a CLAUDE.md
asserting "gitleaks and the PII gate will catch it". The gate was retired
earlier today by Lukas's decision (backend#1409); this line was not updated
with it.
That is a violation of this file's own rule, which I wrote: "If your change
makes a statement in any CLAUDE.md, BUGBOT.md, or runbook false, update that
file in the same PR." I retired the gate and left the canon claiming it
protects - and this canon syncs into every repo's CLAUDE.md, so the false claim
was queued to propagate to 19 repos.
The replacement text does not just delete the reference; it names the resulting
gap. gitleaks still covers secrets in CODE. Nothing now scans PR titles,
descriptions or commit messages, which is exactly what the retired gate did and
exactly where a customer name is easiest to leak on a public repo. A reader who
only saw "gitleaks catches it" would reasonably assume the prose is covered
too.
Refs backend#1630, backend#1409.
…taging" (backend#1592 step 1)
Step 1 of the three-step rename. Nothing changes behaviourally: this teaches all
five reusables to prefer the NEW column name and accept the OLD one, so they
work on both sides of the rename instant. Step 2 is the UI rename (Lukas), step 3
removes the fallback.
WHY A FALLBACK RATHER THAN A SWAP. A rename cannot be done reads-before-writes.
Adding a column can - teach everything to read the new name, then start writing
it - but a rename makes the old name stop existing at a single instant, with no
period where both are present. Every `select(.name=="FR on staging")` would
resolve to null on the far side of that instant, and null resolution in these
workflows means "abort" or, worse, "skip silently". So each resolver asks for the
new name and falls back to the old, and each comparator accepts both.
WHY THE UI, NOT THE API - and this is measured, not cautious. The ticket records
that `updateProjectV2Field` replaces the whole option set and re-mints every ID:
on 2026-08-05, adding ONE option preserved 0 of 11 option IDs and blanked 625 of
625 items, recoverable only because a name-keyed backup existed (#1576). The UI
renames in place, preserving the option ID and every item's value. Step 2 is
therefore deliberately a human action.
Changed, by kind rather than by file:
RESOLVERS (name -> option id), given a new->old fallback:
advance-deploy-env.yml, kanban-closure-router.yml, kanban-reconcile.yml
(via a new opt_either helper)
COMPARATORS (status name read back from the board), now accepting both:
advance-deploy-env.yml override map + rank(), fr-gate.yml rank(),
kanban-reconcile.yml x3, kanban-closure-router.yml, fr-pass-comment.yml
WRITE SITES, now naming the new column:
kanban-closure-router.yml staging branch, advance-deploy-env.yml staging
EXISTENCE PROBE:
fr-pass-comment.yml resolves whichever staging-review column exists and
keeps its loud failure for the case where NEITHER does - which is what that
block was written for, and its comment already anticipated this rename.
Both rank() functions score the two names identically, so the monotonic
never-demote guard cannot see a rename as a backwards move.
VERIFIED against live board data in both states, not just reasoned about:
today (only the old name exists): opt_either -> d3caf1d4
after rename (name swapped): opt_either -> d3caf1d4
live id of "FR on staging": d3caf1d4
Same id either way, which is exactly what the in-place UI rename preserves.
18 literal references remain in comments and user-facing message strings. Those
are correct as they stand today and become wrong only after step 2, so they are
step 3's work rather than a partial edit now.
Removes the public PII gate in full, per Lukas (2026-08-10). The gate was
already inert — parked under backend#1409 (PII_DENYLIST was never provisioned,
so it failed closed on every public-repo PR while being a required check
nowhere) and its public callers were removed org-wide. This deletes what
remained:
- .github/workflows/public-pii-gate.yml (the reusable gate)
- .github/workflows/public-pii-gate-selftest.yml (its decision-table selftest)
- scripts/tests/pii-gate-selftest.sh (the selftest harness)
Also fixes the references that would otherwise dangle at deleted files:
- code-quality-caller.yml / code-quality.yml: drop pointers to the removed
gate + selftest
- version-bump-gate.yml: keep the override-ordering + grep-error lessons but
past-tense them ("the retired public-pii-gate")
- caller-drift.py: the visibility-mismatch finding no longer claims the pii
gate is required
Kept as history: the repo-inventory retirement tombstone and the
conformance-gate defect-class lesson (both reference backend#1409, not a live
file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(claude): sync org-standards block (backend#1602)
chore(ci): retire the public PII gate everywhere
docs(standards): the PII gate no longer exists, so stop promising it (backend#1630)
…ed-totalcount
fix(kanban): fail-closed closing-issue lookups + totalCount (Bugbot .github#166)
…-fallback
feat(kanban): resolve "Staging (human review)", fall back to "FR on staging" (backend#1592 step 1)
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment thread.github/workflows/advance-deploy-env.yml Outdated
@tracebloc-release-traintracebloc-release-trainBot 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 10, 2026
…hipped (backend#1563 Layer 2) (#189)
* feat(release): open a version bump PR when the declared version has shipped (backend#1563 Layer 2)
The train reads version_file as the DECLARED NEXT RELEASE, cuts vX.Y.Z from it,
and never writes it back. So the instant a release succeeds the file names the
version that just shipped, and the NEXT prod hop hits version_preflight's
"v$ver already exists and the delta touches published files" refusal. Every
successful release arms the next one to fail (backend#1440). It stalled the
tracebloc-py-package prod leg on 2026-08-05 and cost a release leg plus a hand
diagnosis before anyone realised the train was right and the file was stale
(backend#1561).
Layer 1 (.github#172) cannot catch this case. It fails a PR whose author changed
published files without bumping - but here nobody authored anything. The
staleness is created BY the release.
VERIFIED AGAINST LIVE STATE, and it found the condition already present:
tracebloc-py-package develop declares 0.18.0
highest released final tag v0.18.0
-> already shipped; this would propose 0.18.0 -> 0.18.1 today
So the pilot repo is sitting in exactly the state that stalls the next hop.
WHAT IT DOES NOT DO. It never tags and never publishes. backend#1563 names "two
competing publishers" as the failure to avoid, and the train owns the prod tag
plus the release-managers token in the tag ruleset. This opens a PR; a human
merges it. That is also why release-please was not adopted - we want its "open
the bump PR" half and specifically not its "tag and publish" half. A PR rather
than a push because develop is protected everywhere, and a push would need admin
bypass, making this a second unreviewed writer to develop.
Patch is a FLOOR, not a judgement: it unblocks the hop, and a human wanting a
minor or major edits the PR. Computing the bump from commit types needs
Conventional Commits, which #1563 sequences separately and no repo has adopted.
Parsing is read_version's case table copied rule for rule, including the
trailing `head -1` on every branch - `grep -oE` prints one line per MATCH, so a
version line with a trailing comment otherwise yields a two-line value
(release-train#41). A parser that disagreed with the train's would propose a
bump the train then rejects.
It refuses rather than guesses: a missing or unparseable version file, an
unreadable tag list, an unreadable compare, a released version that is not
X.Y.Z, or a write that does not read back as intended all exit non-zero with a
named reason. The read-back check matters most - a sed that silently matched
nothing would otherwise open a PR containing no change at all.
Does nothing in the common cases: declared already ahead of every release, no
publishable path touched since the release, or a bump PR already open.
Caller for the py-package pilot follows once this reaches main - a caller pins
@main, and passing inputs a @main callee does not declare kills the whole call
with startup_failure.
* fix: refuse at the compare file cap instead of reading it as "nothing published changed"
Bugbot, .github#189. The compare endpoint caps `files` at 300 and does NOT
paginate that array - `--paginate` walks commits, not files - and there is no
truncation flag. So "no publish-path match" was not something this could
assert, and asserting it exits 0 and SKIPS the bump PR, leaving exactly the
stall this workflow exists to clear. A silent skip is the worst possible
failure for this particular job.
Layer 1 already refuses at that cap. The two were inconsistent, which is the
thing I keep insisting on avoiding: a guard that disagrees with the gate it
front-runs is worse than no guard, and here the two halves of the SAME ticket
disagreed about what a truncated compare means.
Three changes, all mirroring Layer 1 rather than inventing a second rule:
- EMPTY file list is now a refusal. GitHub omits `files` entirely on very
large compares, so empty cannot be told apart from "nothing changed", and
zero files scanned is a malfunction rather than a clean answer.
- ONE LINE PER FILE, tab-separated (filename + previous_filename), instead of
one line per path. The cap check counts lines, and emitting two lines for a
rename would make it refuse at fewer than 300 real files. Verified: three
files including one rename counts as 3, not 4.
- THE CAP ITSELF: 300 files with no match refuses. Note the asymmetry, which
is why the check sits only on the no-match branch - a MATCH is sound on a
truncated list, so a large compare that does match still gets a precise
answer instead of a refusal.
The rewrite also fixes a bug Bugbot did not mention: the old loop was
`for f in $FILES`, which word-splits, so any path containing a space became two
bogus entries. It now reads tab-separated fields, and matches the pre-rename
path too - a file moved OUT of a published tree left the package, and the
compare reports only its new path in the entry.
Verified: actionlint clean, shellcheck clean at error level, `bash -n` clean on
the extracted script, and the heredocs resolve to column 0 after YAML strips the
block indentation.
* fix: require a PAT for the bump PR — GITHUB_TOKEN-authored PRs get no checks
Bugbot, .github#189, and it defeats the entire design rather than degrading it.
Events authored with GITHUB_TOKEN do not start workflow runs. So a bump PR
opened with the ambient token gets ZERO checks - and on a repo with required
checks it is unmergeable forever. Measured on the pilot repo:
tracebloc-py-package develop requires 6 checks:
tests (3.11), tests (3.12), quality / gitleaks,
quality / house-rules, quality / ruff, quality / shellcheck
None of them would ever report. The PR would sit until someone merged it with
admin bypass - which is exactly the thing this workflow opens a PR to avoid. It
would have swapped "a push needing admin bypass" for "a PR needing admin
bypass", while looking safer.
The release train hit this first and its comment says why: "Bugbot only
auto-reviews HUMAN-authored PRs, and settle/release now require a review". It
uses RELEASE_TRAIN_PR_TOKEN. Same fix here, same reasoning, same secret
available.
- `pr-token` is a REQUIRED secret, deliberately not defaulted to
github.token. A default would reintroduce the bug silently in any caller
that forgot it.
- checkout takes the PAT, so the credential the push uses belongs to it -
what decides whether a push starts runs is the identity that performed it,
not the commit author.
- Only the two WRITE operations use it, named at their call sites the way
promote-repo.sh does. Reads stay on the ambient token.
- An empty token refuses UP FRONT, before the branch exists, rather than
after - discovering it later leaves a pushed branch and no PR.
Verified: actionlint clean, bash -n and shellcheck clean on the extracted
script, and the workflow_call contract parses with pr-token required: true.
* fix: an empty compare has two opposite causes — stop treating both as failure
Bugbot, .github#189. I collapsed "nothing has landed since the release" and
"GitHub omitted the file array" into one hard error. They are opposites:
ahead_by == 0 $BASE holds nothing the tag does not. Right after a release
that is the NORMAL state, and there is genuinely nothing to
bump.
files omitted dropped entirely on very large compares. Nothing was scanned,
so no conclusion about published paths is available.
Treating the first as a malfunction fails this job RED on an ordinary
post-release day. Treating the second as clean skips a bump that is owed. The
previous commit did the former, which is the noisier half of exactly the
distinction I added the truncation guard to make.
Fixed with `ahead_by` from the same response, confirmed by `files` being NULL
(absent) rather than []. Verified on real compares:
v0.18.0...v0.18.0 status=identical ahead_by=0 files=present(0)
-> exits 0 quietly (previously: RED)
v0.18.0...develop status=diverged ahead_by=3 files=present(9)
-> proceeds, scans 9 files
Note the ordering matters and the test found why: an `identical` compare
returns files as a PRESENT but EMPTY array, so the absent-check would not have
caught it. `ahead_by` has to be consulted first.
The compare is now fetched ONCE and kept. Three questions are answered from it
- ahead_by, files-present, and the file list itself - and re-fetching could
observe three different states.
Also handles ahead-by-real-commits-with-an-empty-files-array (empty or
merge-only commits): nothing published can have changed, so that is a clean
"nothing to do" rather than a refusal.
* fix: refuse empty inputs — an empty publish-paths silently skips every bump
Bugbot, .github#189. `required: true` on a workflow_call input still permits "".
With an empty publish-paths, `for pat in $PUBLISH_PATHS` never iterates, MATCHED
stays empty, and the job exits 0 as though nothing publishable changed - so a
caller that forgot the input gets a workflow that runs, reports success, and
never opens a bump. Demonstrated:
PUBLISH_PATHS="" -> MATCHED=<empty> (exits 0, skips)
PUBLISH_PATHS="tracebloc/* pyproject.toml" -> MATCHED=hit
Layer 1 refuses the same case, in the same words: absent is never permission to
skip the version guard (backend#1439). Mirrored here rather than invented.
Guarded version-file and base-branch too, on the same reasoning. An empty
version-file previously fell through to the `[ ! -f "" ]` check and refused with
"does not exist", which is true but points the reader at the wrong problem.
This is the FOURTH finding on this PR and the fourth of the same species: an
empty or absent thing being read as a verified negative. Truncated compare read
as clean. Empty compare read as failure. And now an empty input read as "nothing
matched". The logic each was wrapped around was fine every time.
* fix: read-only permissions — a write grant would hide the mistake pr-token guards
Bugbot, .github#189. The permissions block granted contents:write and
pull-requests:write to GITHUB_TOKEN, but nothing uses the ambient token to
write: the branch push uses the credential checkout persisted from pr-token,
and gh pr create names PR_TOKEN explicitly. Audited every call - the three
ambient-token operations are matching-refs, compare and pr list, all reads.
So the grant bought nothing and cost the fail-fast. A future edit that forgot
to name PR_TOKEN would SUCCEED against the ambient token instead of taking a
403 - and that produces a PR authored by GITHUB_TOKEN, which starts no workflow
runs, so its required checks never report and it can never merge. That is
precisely the failure the pr-token guard was added to prevent two commits ago.
A permissions block that lets it through quietly is worse than one that stops
it loudly.
It also contradicted the comment directly above it, which claims reads run on
the ambient token for least privilege. The comment was right and the grant was
not.
Now contents:read + pull-requests:read, with the reasoning recorded at the
block rather than in a commit message nobody will find.
* fix: the idempotency probe refuses with a named reason like its siblings
Bugbot, .github#189. The open-PR check used a bare assignment while
matching-refs and compare both use `if ! var=$(...)` with an ::error::. Under
set -e the bare form still fails the step, so this is about the OPERATOR rather
than about safety: the run died at an unnamed line, breaking this file's own
"every failed read exits with a named reason" contract.
The reason matters here more than the consistency does. An unreadable PR list
is specifically dangerous to guess at: assuming "none open" would open a SECOND
bump PR alongside the existing one, and two PRs proposing different versions of
the same file is worse than none.
Audited the rest while in there - 3 guarded reads, 0 bare `gh` assignments left.
* fix: measure the delta from the DECLARED version's tag, not the latest release
Bugbot, .github#189. version_preflight refuses on "v$DECLARED already exists AND
the delta touches published files" - it keys on the DECLARED version. I was
measuring from v$LATEST, which asks a question the train never asks.
The two are the same tag in the common case (declared == latest) and diverge
exactly when the file has fallen BEHIND a release - reverted, or a stale branch.
Measured on the pilot repo, which shows the size of the gap:
DECLARED=0.18.0 (== latest) -> base v0.18.0, develop ahead_by=3
DECLARED=0.17.0 (behind) -> base v0.17.0, develop ahead_by=15
Five times the window. In the behind case the old code inspected 3 commits and
could answer "nothing publishable changed" about a window the train is not
looking at, while version_preflight tested 15 commits' worth of delta against
v0.17.0 and stalled.
Falls back to v$LATEST when the declared version is not itself tagged: it is
<= LATEST and unreleased, so the newest thing that IS released is the honest
baseline for "what changed since we last shipped".
All eight compare messages now name the actual base rather than hardcoding
v$LATEST, so the log says which tag the answer is about.
* fix: derive the declared tag's existence from the guarded read, not a second probe
Bugbot, .github#189 - and this one I introduced in the previous commit while
fixing finding #7.
That commit added `gh api git/ref/tags/v$DECLARED >/dev/null 2>&1` and treated
ANY non-zero as "not tagged". A 404 means untagged; a rate limit or a 5xx means
unknown. Collapsing them makes a transient failure silently select v$LATEST as
the compare baseline - which is exactly the wrong-baseline fail-open that
finding #7 was about. I closed a fail-open by adding a smaller one.
The fix is not a better probe, it is no probe: $REFS from the matching-refs read
above ALREADY lists every v* tag, and that read already refuses on failure. So
the question is answered from data whose failure mode is handled, and there is
no second failure mode to get wrong.
Uses a here-string rather than `printf | grep -q`: under pipefail grep -q closes
the pipe on its first match, the producer takes SIGPIPE, and a real hit returns
141 and reads as a miss. That is the .github#173 defect, and piping here would
have reintroduced it in a place where a miss silently changes the baseline.
Verified against real tag data: v0.18.0 and v0.17.0 resolve to themselves,
v0.99.0 falls back. One matching-refs call total; the extra probe is gone.
@tracebloc-release-traintracebloc-release-trainBot 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 10, 2026
…kanban card (#193)
Bugbot .github#190 (Medium). The projectItems totalCount>20 guard only warned,
so if an issue is on >20 projects and its engineering-kanban card sits beyond
the first 20 we fetch, ITEM_ID comes back empty and the card is silently
skipped as "not on project" while the run stays green — the exact silent-skip
the guard was meant to close (the sibling closingIssuesReferences guard already
fails closed).
Move the truncation check into the not-found branch and set RUN_FAILED there:
we only fail when the card was actually not found AND the list was truncated
(pi_total>20), so a normal issue-on-<=20-projects still skips cleanly with no
false failures.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@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 b04c241. Configure here.

Comment thread.github/workflows/fr-pass-comment.yml
@tracebloc-release-traintracebloc-release-trainBot 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 10, 2026
@tracebloc-release-train
tracebloc-release-trainBot merged commit d09d208 into stagingAug 10, 2026
23 checks passed
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