Skip to content

release-train: staging -> main - #186

Merged
tracebloc-release-train[bot] merged 37 commits into
mainfrom
release-train/to-main
Aug 10, 2026
Merged

release-train: staging -> main#186
tracebloc-release-train[bot] merged 37 commits into
mainfrom
release-train/to-main

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-main branch (a mirror of staging), so it never collides with a human PR. Merged only when the fr-gate is green.


Note

High Risk
Wide blast radius on kanban deploy automation (token-backed GraphQL writes), removal of the public PII gate, and new fail-closed gates that can block merges or train promotions if misconfigured.

Overview
This promotion ships a large batch of tracebloc/.github automation and governance changes that affect every repo consuming these reusables.

Kanban / deploy advancement renames the staging review column to Staging (human review) (with fallbacks for FR on staging during the board rename) across advance-deploy-env, closure routing, FR pass, reconcile, and related rank logic. advance-deploy-env gains a dry_run input and now advances issues closed by promoted PRs (including cross-repo), with stricter fail-closed handling for lookup errors and pagination limits.

Release versioning (backend#1563) adds reusable version-bump-gate (PR-time check aligned with release-train version_preflight) and version-bump-pr (opens a bump PR when develop is stale after a release), plus a selftest workflow. README onboarding now tells train repos to wire the gate caller.

Org contract enforcement adds conformance-gate (required check that polls caller-drift when repo-inventory.yml or drift tooling changes), standards-sync with org-standards.md / scripts/standards-sync.py, and a managed block in this repo’s CLAUDE.md. repo-inventory.yml drops public-pii-gate everywhere, marks .github as running code-quality, and sets e2e-test-agentrelease_train: true.

Other changes:code-quality-caller.yml so this repo runs its own quality gate (including hard action-pins); kanban-reconcile applies keep-open to open North Stars issues before stale sweep; customer-priority-bump is simplified to label-only (no project field inputs); public-pii-gate workflow and caller are removed (documented as retired; PR prose scanning is no longer automated).

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

LukasWodkaand others added 26 commits August 6, 2026 08:39
…ks got impossible advice (backend#1409) (#154)
* fix(pii-gate): the override could not rescue an unset secret, and forks got impossible advice (backend#1409)
Two defects, plus the first test this gate has ever had.
1 · THE DOCUMENTED ESCAPE HATCH DID NOT WORK
The `pii-gate-override` label was checked AFTER the unset-secret refusal, so on
a PR where PII_DENYLIST is missing the refusal exited first and the label could
not rescue anything. Both this file's own header ("Override a false positive:
add the 'pii-gate-override' label") and backend#1409's remediation note ("or
apply the pii-gate-override label to an individual PR") advertise it as the
per-PR way out of exactly that state.
That state is not hypothetical: the secret still does not exist, and today's
`.github` promotion (8aabe41, 13:53) armed the fail-closed path, so all seven
public repos now carry a red check that the documented workaround cannot clear.
An override is a statement that this PR should not be evaluated, so it now
precedes every reason the evaluation might refuse.
2 · A FORK PR WAS TOLD TO GO SET A SECRET THAT CANNOT HELP IT
GitHub does not pass secrets to `pull_request` runs from a forked repository, so
DENYLIST is empty on any fork PR regardless of what the org secret contains. The
gate then took the unset-secret path, whose message asks an org admin to run
`gh secret set` — advice that cannot work, because this is a property of the
event and not of the configuration.
Forks now get their own branch with a true reason and a real remedy: a
maintainer reads the PR text and applies the override label to record it. Still
fails closed. Compared against exactly "true", so an absent head.repo (deleted
fork) falls through to the paths below, which refuse on an empty denylist and
refuse again on a Compare read they cannot complete — an unknown fork status
cannot buy a pass.
This also unblocks #1409 defect 2. Making `pii-gate / pii-check` required would
have made every fork PR unmergeable; with the fork path explicit, the check can
be required for same-repo PRs and deliberately not for forks. Measured today:
0 fork PRs across all 7 public repos, so nothing is affected retroactively.
3 · A SELFTEST, IN THE SHAPE caller-drift ALREADY USES
scripts/tests/pii-gate-selftest.sh extracts the gate's `run:` block and executes
it against a stubbed `gh`. 15 cases: both new behaviours, the unset-secret and
fork refusals, title/body/commit matching, and every fail-closed path (comma-only
denylist, truncated commit list, unreadable Compare, missing base SHA, glob-shaped
term), plus a regression case for defect 3 — an early match in a ~150KB haystack,
which is the shape that used to be discarded via SIGPIPE.
Every case asserts the exit status AND a distinguishing phrase, because a fork PR
and an unconfigured org both exit 1 and the whole point of this change is that
they must not say the same thing. A status-only test would pass while the gate
gave impossible advice.
Proof the suite bites rather than merely being green: run against the currently
live main copy it fails exactly the 4 new cases and passes the other 11, so
nothing pre-existing regressed.
The test depends on the `run:` block staying free of `${{ }}` interpolation. That
is asserted, not assumed — extraction fails loudly if a future edit inlines an
expression, rather than silently covering less.
Not fixed here, and still open on backend#1409: the secret itself does not exist
(a content decision), and the check is required on 0 of 7 public repos. Scanning
the diff rather than only PR metadata is backend#1559.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(pii-gate): pad the regression haystack via commits, not a 150KB env var
The defect-3 case exported a ~150KB PR_BODY. Linux caps a single exec
argument or environment string at 128KB (MAX_ARG_STRLEN), so every exec
after that export died with E2BIG -- grep, head, tr and even the trap rm.
macOS has a larger limit, so it passed locally and failed on the runner:
"/usr/bin/grep: Argument list too long".
Also unrealistic. GitHub caps a PR body at 65,536 characters, so no real
run could produce that env var. Padding now comes from 250 commit
messages generated straight into the fixture file, which never travel
through argv or the environment -- and that is the faithful shape anyway:
backend#1409 describes the haystack as title + body + up to 250 commit
messages, title first.
The case now asserts the haystack size (80,390 bytes) exceeds the 64KB
pipe buffer. Without that, the fixture could shrink and the case would
stop reaching the defect while staying green -- a regression test that no
longer reproduces its bug is the same class of false comfort #1409 is
about.
Proof it reaches the defect: reverting the matcher to the pre-#130 pipe
form makes this case, and only this case, fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ep-open sync (backend#1597) (#163)
stale-backlog is column-blind: a quiet North Stars epic auto-closes
after 6+8 weeks and routes to Cancelled. The sweep already exempts
keep-open - this keeps that label synced from board state nightly.
One-way by design; write failures fail the step.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ly does (backend#1597) (#164)
project-number, org and target-priority were declared and never read;
the step hard-codes the 'priority' label per D5. Measured all 16 callers
on develop and prod branches: byte-identical, secrets: inherit only, no
with: blocks - so outright removal breaks nothing. trigger-label stays
(used in the job if:). Header comment now describes the actual behavior:
trigger label in, binary 'priority' issue label out, no board field.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…4) (#162)
release-train#34 merged 2026-08-06 and listed the repo, so release_train can now
be true. It could not be set in the same PR as the caller flip (.github#153):
caller-drift.py:562 verifies this field against release-train/repos.yml, so
flipping it early fails the guard.
RFC-BACKEND-1552 D2 is the decision; it amends RFC-BACKEND-1405 D1, which had
named this repo in its exempt list.
Verified: caller-drift exits 0, 20/20 repos, no drift.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…d skips CLOSED issues (.github#166) (#167)
* sec(ci): SHA-pin checkout+setup-python in the pii-gate selftest (backend#1491, D10)
The new selftest workflow used floating @v4/@v5 while every other workflow here
pins to a 40-char SHA; this repo runs no code-quality caller to catch the drift.
* fix(reconcile): North Stars keep-open shield skips CLOSED issues (Bugbot .github#166)
The shield filtered on status+type but not issue state, so closed North Stars
issues got a sticky keep-open the classify step may then terminalize.
… has one (backend#1586) (#169)
The WHY-THIS-FILE-EXISTS note closed with a present-tense claim - "there is
no closure caller there to route it" - that the body of the same file now
contradicts. e2e-test-agent's entry reads
kanban-closure-router.yml: required # kanban-closure-routing.yml
since the e2e-test-agent#13 remediation, and the caller is really there:
tracebloc/e2e-test-agent@develop ships .github/workflows/kanban-closure-routing.yml
resolving to tracebloc/.github/.github/workflows/kanban-closure-router.yml@main.
Note the filename - `kanban-closure-routing.yml`, not `-caller.yml` - which is
exactly the mismatch the MATCHING IS ON `uses:` CONTENT paragraph below warns
about, and exactly why a reader checking the header's claim by filename would
have concluded it was still true.
This is a file whose entire purpose is being the single source of truth that
caller-drift.yml enforces. A header that misstates the live state of a repo
the body governs is the one defect it cannot afford, even in a comment: the
guard checks the entries, nobody checks the prose, so the prose is where a
stale claim survives longest.
The fix keeps the motivating history and moves it into the past tense, because
e2e-test-agent#1 really did close without routing and that is the evidence
backend#1415 was built on. What changed is not the diagnosis, it is the repo:
the motivating gap is closed, the detection gap the file exists to fix is not.
Zero runtime impact - a comment. YAML parse re-verified (20 repos load).
Found on the 2026-08-05 develop->staging hop (.github mirror #155,
repo-inventory.yml:444), landed to staging under triage. Parent backend#1405.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…1603) (#171)
* feat(ci): run the org's own code-quality suite on this repo (backend#1603)
This repo publishes code-quality.yml -- and its action-pins job -- to all 19
other repos, and was the only repo that never ran it. The exemption was
invisible because actionlint.yml looks like coverage: it type-checks every
expression, validates needs/uses wiring, and shellchecks every run: block.
What it does not do is look at action REFS. So the pinning rule this repo
defines was the one rule this repo was not subject to.
That is not theoretical. On 2026-08-06 .github#168 -- an otherwise correct
fix -- reverted actions/checkout from its pinned commit SHA back to the
mutable @v4 tag, in advance-deploy-env.yml, the most-consumed reusable
workflow in the org and one that runs with PROJECTS_KANBAN_TOKEN in scope.
action-pins exists to fail exactly that diff. It did not run. A human reading
the diff caught it, which is the review path D10 was written to stop
depending on.
Armed from day one rather than soft-failed into a backlog, because the tree
was measured first: shellcheck --severity=error clean over both shell
scripts, house-rules.sh clean over the same, and 24 `uses:` refs across 23
workflows with zero pin violations. ruff and gitleaks could not be run
locally and get their first honest look on this PR; whatever they surface is
fixed or baselined here rather than the gate being softened -- the same
bargain e2e-test-agent's caller records in its own comment.
action-pins-soft-fail is set to false explicitly rather than left to inherit
soft-fail. It is the check whose absence let #168 through, and every other
repo consumes these workflows at @main and inherits whatever refs they pin,
so this repo has a specific duty to enforce it on itself.
actionlint.yml stays separate. The two gates have deliberately different
postures -- actionlint blocks from day one against a tree cleaned in the same
change; code-quality ships soft-fail: true for repos still clearing backlog
-- and folding them together would force one posture onto both.
No paths: filter, so the check can be made required without leaving
workflow-free PRs waiting on a status that never reports.
Refs backend#1603, backend#1491 (D10), backend#1276.
* fix(ci): drop the action-pins inputs -- @main does not declare them yet
First push of this branch produced a startup_failure on the reusable call
(run 31086491251): every other check reported normally and `Code quality`
never started. Cause: `action-pins` and `action-pins-soft-fail` were added in
#159, which is on `develop`; `main` still carries the five-job version of
code-quality.yml. Callers must reference @main (Q3), and passing an input the
@main callee does not declare fails the entire call before any job runs.
Same merge-order trap as the #160 audit red: a change becomes consumable by
@main callers when it is PROMOTED, not when it is merged.
The other five jobs still run armed, so most of backend#1603 closes now. The
arming diff is written verbatim in the file so the follow-up is mechanical
once #159 reaches main.
Refs backend#1603.
… (backend#1600) (#168)
* fix(kanban): advance the ISSUES each promoted PR closes, not just the PR (backend#1600)
advance-deploy-env advanced only PRs; issues closed by a develop-merged PR were
parked at On dev by the closure-router and never advanced when their code shipped,
drifting permanently. For each PR in the push, resolve closingIssuesReferences
(cross-repo aware) and advance each closing issue with the same monotonic guard,
archived-skip and (new) dry-run behaviour. Adds a dry_run input; update_field
short-circuits under it so both the PR and issue paths can be tested without writes.
* fix(kanban): only advance CLOSED closing-issues (Bugbot .github#168 High)
PRs merge to develop (not the default branch), so closingIssuesReferences lists
still-OPEN issues; advancing those into deploy columns is wrong and reconcile
bounces them to Backlog. Gate the advancement on issue.state == CLOSED.
* sec(ci): restore the actions/checkout SHA pin (backend#1491 D10)
This branch changed `actions/checkout@11d5960a…` to the mutable `@v4` tag in
advance-deploy-env.yml — the most-consumed reusable workflow in the org, and
one that runs with PROJECTS_KANBAN_TOKEN in scope. A floating major tag can
be repointed by the upstream owner at any commit, which is exactly what D10
pinning removes.
Nothing in CI would have caught it: tracebloc/.github runs only its own
actionlint.yml, which does not inspect action refs, and it does not call the
code-quality workflow whose action-pins job exists to fail this diff. That
gap is now backend#1603 (.github#171, merged) — but action-pins itself cannot
arm here until #159 reaches main, so for the moment this restore is the only
thing standing between the unpin and every repo that consumes these
workflows at @main.
No functional change to the backend#1600 fix.
…oved to PR time (backend#1563 Layer 1) (#172)
* feat(ci): version-bump gate — the train's prod-hop refusal, moved to PR time (backend#1563 L1)
The release train already refuses to publish changed bytes under an
already-released version (promote-repo.sh:version_preflight). Correct, and
days late: backend#1561 spent a whole release leg discovering that
py-package's pyproject.toml still said 0.17.0 while v0.17.0 was tagged and
the delta touched tracebloc/*.
This asks the same question on the PR that causes it.
delta touches publish_paths AND v<version-on-this-PR> is already tagged -> red
Not "the version file changed vs base", which #1563 words it as: that would
demand a bump on every published-path PR, and one bump has always covered a
whole release cycle. A check that is wrong on the common case gets removed.
Parity is the point, so read_version and publishable_delta are copied from
promote-repo.sh rule for rule — the per-extension anchors included, since
those are what keep appVersion, a dependency pin and a leading comment out of
the answer. Two deliberate divergences, both strictly tighter: `set -f` (a
publish path is a glob, and promote-repo.sh only survives it by running where
no publish path exists), and one paginated tag listing instead of a
per-version git/ref probe that cannot tell 404 from 403.
Fails closed on every unreadable input, and `soft-fail` does not reach any of
them: it downgrades the verdict, never the machinery. Zero files scanned is a
malfunction. No paths: filter and no job-level if:, so a required check can
never go missing on a PR.
45-case decision table in scripts/tests/, run offline against a stubbed gh,
covering every fail-closed path and all five repos' version file formats.
Callers follow in one PR per repo, once this is on main.
* sec(version-gate): a branch name is not a credential — tighten the promotion exemption
Self-review before Bugbot. The release-train exemption keyed on the head ref
alone, and a head ref is chosen by whoever opens the PR: `release-train/anything`
was an exemption anybody could mint, from a fork included. It only skipped a
version check the train re-runs at the prod hop, so nothing could actually
publish through it — but "the gate is disarmed by naming your branch" is not a
property to leave in a gate.
Now all three must hold, and the extra two are facts about a real promotion
rather than claims made by its author: head ref prefix, head in THIS repository
(never a fork), base in staging/main/master. A promotion never targets develop,
so a PR that borrows the name to reach develop is evaluated normally — with a
warning naming the mismatch, because a bypass that does not apply should be
visible rather than silently absent.
Also: an EMPTY version-file read is retried on purpose (it is the exact symptom
of the Contents API lag that left py-package untagged on 2026-07-29), so the
message after three failed attempts now names both possibilities instead of
saying only "could not read".
50 cases, 4 of them new and all four red before this change.
* sec(version-gate): the fork test demands a confirmed false, not merely 'not true'
head.repo is absent on a PR from a deleted fork, so IS_FORK arrives empty and
'not true' read that as 'same repo' — granting the promotion exemption on a
field nobody could read. Exempting on an unread field is the shape of every
fail-open in backend#1409.
Three-way now: false exempts, true says fork, anything else says the head
repository could not be confirmed. Evaluating a genuine promotion by mistake
costs a re-run; exempting a fork by mistake costs the gate.
* fix(version-bump-gate): head -1 the version parse; grep -oE prints per match
Bugbot, .github#172. The yaml/toml/py branches take `head -1` on the LINE,
then run `grep -oE "$semver"` on it -- and grep -oE prints one line per match,
not per input line. A version line carrying a second semver in a trailing
comment
version = "1.2.3" # bumped from 1.0.0
therefore parses to the two-line value "1.2.3\n1.0.0". Measured, not
theorised.
Here that corrupts the regression guard: `sort -V | tail -1` answers 1.2.3
while `"$HEAD_VER"` is the whole two-line string, so the `!=` compare is true
and the gate refuses a perfectly legitimate bump. The bare-file branch already
guarded this with a trailing head -1; the other three did not.
The json branch is safe -- jq emits a single value and the pattern is anchored
with ^ -- so it is left alone rather than given a cosmetic head -1.
This same defect is in promote-repo.sh:read_version, which this file copies
rule for rule; it is fixed there in release-train#40. In the train the
consequence is worse than a false refusal: the parsed value is interpolated
into the tag-probe URL, a malformed URL 404s, and the caller reads that as
"this version is not released" -- fail-OPEN on the one check that stops a
re-publish under an existing tag.
…(backend#1602) (#170)
* feat(standards): org-standards canon + CLAUDE.md sync guard (backend#1602)
One canonical org-standards.md — the org's working rules — stamped into
every active repo's CLAUDE.md between org-standards markers, so every
Claude Code session in every repo loads the same rules.
standards-sync.py audits the fleet develop-first (weekly report-only;
a create-prs dispatch opens per-repo sync PRs against develop), with
caller-drift's design rules: never all-clear from a failed read,
absence never implicit (devex-bootstrap exempt with written reason),
malformed markers reported and never auto-spliced. Offline selftest:
19 checks over the splice logic and every fail-closed path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(standards): install PyYAML in the selftest job; exact-ref branch existence (backend#1602)
Both from Bugbot on .github#170. The selftest's good-path load_targets
check parses YAML, and a clean setup-python has no PyYAML — the job died
at the final check (mirrors caller-drift's selftest job now). And
resolve_branch now uses the exact-match git/ref/heads/develop lookup,
the same endpoint remediate() trusts for the base sha. Verified live:
backend resolves develop, release-train falls back to main.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(standards): a guard crash exits 2 (could not evaluate), never 1 (drift) (backend#1602)
Bugbot on .github#170: Python exits 1 on an unhandled traceback, and 1 is
reserved for confirmed drift — so a malformed API payload or filesystem
error produced a drift report instead of the unknown-state failure. The
entry point now maps any crash to exit 2 with an ::error:: line. Pinned
by a selftest that runs the guard with gh stripped from PATH.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Managed sync from tracebloc/.github/org-standards.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on a failed audit (#173)
* feat(ci): required conformance gate — a contract change cannot merge on a failed audit
caller-drift.yml has run on PRs touching repo-inventory.yml since backend#1415,
and it correctly fails when the inventory disagrees with reality. But it is not
a required status check: .github/develop requires only actionlint. So a PR that
adds a repo to the train with drift went RED and could still be merged. The gate
existed in trigger form and had no teeth.
It cannot simply be marked required as-is. It carries a paths: filter, and a
path-filtered required check never reports on PRs that miss the filter, leaving
them pending forever - the trap actionlint.yml's own header documents. Dropping
the filter would instead run a 20-repo API audit on every PR in this repo, which
is the reason the filter is there.
So this adds a job that ALWAYS runs and ALWAYS reports. On a PR that does not
touch the contract it is green in seconds with zero audit API calls. On a PR
that does, it requires caller-drift's verdict for that exact head sha.
It keys on the WORKFLOW FILE, not the check name. Check-run names are not unique
across workflows - caller-drift.yml and standards-sync.yml both expose a job
called selftest - so polling by name would happily accept the wrong workflow's
verdict. Querying actions/workflows/caller-drift.yml/runs?head_sha=... cannot be
confused that way.
Fail-closed on every non-success branch, each with a named reason: no run found
within the poll budget, a non-success conclusion, an unreadable API, or a
changed-file list that came back empty (unreadable, not "no files"). The
--paginate on the files query matters: a PR over one page would otherwise look
like it touches only its first 30 files, and a truncated read reporting "not
guarded" is precisely the silent pass this file exists to prevent.
The path match is exact (grep -qxF), verified offline against the cases that
matter: docs/repo-inventory.yml and repo-inventory.yml.bak do NOT count as
touching the contract.
This PR edits repo-inventory.yml's header on purpose, so it exercises its own
guarded path rather than shipping a gate whose interesting branch never ran.
Refs backend#1608, backend#1415.
* fix(inventory): .github's code-quality caller is required, not exempt
Caught by the gate this PR adds, on its own first run. The audit reported:
.github: code-quality.yml is marked `exempt` but a caller exists
(code-quality-caller.yml). The exemption is stale.
Correct, and the drift is an hour old: .github#171 landed the caller for
backend#1603 and never flipped this entry. That is precisely the follow-up
caller-drift.py's docstring tells you to do -- "land the caller first, flip
the entry in a follow-up" -- because the inventory is read from the checkout
while repo state is read from the audit branch, so doing both in one commit
fails.
So the sequence worked exactly as designed: the caller landed, the entry went
stale, and the very next inventory-touching PR was refused until it was
reconciled. That is the whole point of the gate, demonstrated on itself
rather than asserted in a comment.
The exemption text is replaced with the remediation history rather than
deleted, matching how the pii-gate and e2e-test-agent entries record theirs --
the reason the exemption existed is evidence, not noise.
* fix(gate): here-string, not a pipe — SIGPIPE under pipefail turned a hit into a miss
Bugbot, .github#173. Reproduced before fixing:
20,000 filenames (~349 KB) with the guarded path FIRST
printf '%s\n' "$FILES" | grep -qxF repo-inventory.yml
-> pipeline rc 141, `if` takes the else branch, gate reports NOT GUARDED
grep -q exits on its first match and closes the pipe. With enough left to
write, printf takes SIGPIPE and exits 141; pipefail then makes the PIPELINE
141, so a real match reads as a miss. The gate would report "not guarded" and
pass GREEN on precisely the PRs most likely to matter -- the failure gets MORE
likely as the diff gets bigger, and it fails OPEN, which is the one direction
this file exists to rule out.
A here-string is a single command, so the status is grep's own. Verified: same
input, same guarded path, correctly GUARDED.
Swept the rest of the repo for the pattern. fr-gate.yml has two instances
(`echo "$LABELS" | grep -q ...`), and both are safe on two independent counts:
a PR's label JSON is orders of magnitude under the 64 KB pipe buffer, so echo
finishes before grep can exit; and a false negative there means the gate does
NOT skip, i.e. it fails CLOSED. Same pattern, opposite consequence -- left
alone deliberately rather than changed for symmetry.
The `| head -1` chains in version-bump-gate.yml and the kanban workflows are
likewise safe: each is either terminated by `|| true` or fed by output far
under the buffer.
Refs backend#1608, backend#1409 (same defect class in the PII gate).
* fix(gate): renames escaped the guard; poll budget was shorter than what it waits for
Two Bugbot findings on .github#173.
HIGH - a rename escaped the contract guard. The changed-file probe read only
.filename, and a rename reports the NEW path there. So `git mv
repo-inventory.yml elsewhere.yml` sailed through as "not guarded": the
contract file moves out from under the audit and the gate waves it past -
precisely the silent pass this workflow exists to close. Now reads BOTH
.filename and .previous_filename, which is the same reason promote-repo.sh's
publishable_delta matches both fields (a file renamed OUT of a published tree
is still a publish-path change). I had read that comment while building the
version-bump gate and failed to carry it across.
Proven on a synthetic rename payload, since no open PR has one:
old jq (.filename only) -> NOT GUARDED <- the hole
new jq (+ .previous_filename) -> GUARDED
and verified against a real PR that the `// empty` guard emits nothing extra
when previous_filename is absent.
MEDIUM - the poll budget was shorter than the thing it waits for. 40x30s = 20
minutes, while caller-drift's worst case is selftest (timeout 5m) + audit
(timeout 30m) = 35m. A legitimately slow audit would have made the gate go red
as though no verdict ever arrived, blocking a contract change until someone
re-ran it by hand. Fail-closed is right; fail-closed on a budget that cannot
cover the wait is just fail-annoying, and a gate people have to re-run by hand
is a gate they campaign to remove. Budget is now 80x30s = 40m with the job
timeout raised to 45m so the named error message surfaces rather than an
opaque job timeout.
Refs backend#1608.
…backend#1603 step 2) (#178)
The second half of backend#1603. When code-quality-caller.yml landed in #171 it
could not arm action-pins: the job and its two inputs were added in #159 and
existed on `develop` only, and a caller must reference @main (Q3). Passing an
input the @main callee does not declare kills the entire reusable call with a
startup_failure - not the one job, the whole call - measured on that branch's
first push, run 31086491251.
#159 has since promoted, and `main` now carries the six-job version with both
inputs. Verified against the @main copy before flipping, rather than assuming
the promotion carried what it looks like it carried.
Hard-armed rather than left to inherit soft-fail. This is the check whose
absence let .github#168 revert actions/checkout from a pinned commit SHA to the
mutable @v4 tag with nothing in CI to object - in advance-deploy-env.yml, the
most-consumed reusable workflow in the org, running with PROJECTS_KANBAN_TOKEN
in scope. Every other repo consumes these workflows at @main and inherits
whatever refs they pin, so this repo has a specific duty to enforce the rule it
publishes.
Re-measured the tree before arming: 24 `uses:` refs across 23 workflows, zero
violations. Arming imports no backlog.
…-repo default reviewer
@saqlainsyed007 raised both on the sync PRs (backend#1611, averaging-service#308),
and both are source-side: the block must not be hand-edited in a consuming repo.
1. The block opened with an H1 while every consuming CLAUDE.md already has
'# CLAUDE.md' as its sole title. Two H1s break the outline and trip
markdownlint MD025. Every heading is demoted one level so the standards
nest as a section under the host document's title.
2. 'Each repo's CLAUDE.md names its default reviewer' is false fleet-wide --
no repo names one -- and it contradicts the org's actual model, which is
author-picks-reviewer with no automation assigning anyone. The line now
describes what we really do, so the rule above it is actionable.
Selftest: 20 checks, 0 failed.
Co-Authored-By: Claude Opus 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.
Removes this repo's caller for the public PII gate. Lukas's decision,
2026-08-06: retire the gate for now rather than leave it in place undeployed.
WHY. The gate has never had a denylist. PII_DENYLIST does not exist as an org
Actions secret and never has, so the workflow correctly failed closed on every
PR - "an inactive guard must be visible as red, not invisible as green", as its
own header puts it. Applied to a secret that was never provisioned, that
produced a permanent red across all 7 public repos which blocked nothing: 24
open PRs carried it, and the whole org - this author included - had learned to
filter it out. A check that is red on every PR is not fail-closed; it is off
with extra steps, and it would be indistinguishable from noise on the day it
went red for a real reason.
WHAT LAPSES, stated plainly. The gate grepped PR title + body + every commit
message against a private list of customer/partner names, on public repos only.
It never read code, diffs or filenames, and detected no PII patterns - secrets
in code remain gitleaks' job. So what lapses is the mechanical backstop for
CLAUDE.md's rule that internal work and customer mentions never go in a public
repo. That rule still stands and is now enforced by review alone.
The reusable workflow and its selftest stay in tracebloc/.github, dormant with
no callers. Restoring is three steps: set the org secret, restore this file,
flip the inventory entry back to required. Checklist and the denylist-content
caveat are on backend#1409.
Not required on any branch, so this removes no merge gate.
…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.
…d-reviewer-claim
docs(standards): fix the block's H1 and the false default-reviewer claim (backend#1602)
Managed sync from tracebloc/.github/org-standards.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…409) (#183)
Completes the retirement Lukas decided on 2026-08-06. The 7 callers were
removed first (cli#466, client#638, docs#96, data-ingestors#462, model-zoo#130,
start-training#44, .github#182, all merged); this drops the reusable from the
inventory so the contract matches reality.
ORDERING MATTERS AND WAS NOT INCIDENTAL. The audit reads the inventory from the
checkout but every repo's state over the API, so callers and inventory cannot
change in one commit - the docstring says as much for the ADD direction, and the
remove direction has the same hazard inverted. Callers first, then this. In
between, the audit would have reported "MISSING required caller" for 7 repos;
that window is now closed. Verified before pushing by running caller-drift.py
locally against the live fleet: "20 repos x 8 reusables + 2 copies. Audited 20
of 20. No drift."
Removed:
- public-pii-gate.yml from the `reusables` list
- all 20 per-repo `callers` entries for it (7 required, 13 exempt)
- the `private_repo_no_public_exposure` shared reason, now referenced by
nothing
- the stale claim in the MATCHING header that public-pii-gate uses the
`-caller.yml` convention; it no longer has callers to have a convention
The .github entry's long red-inert explanation is replaced by a short
retirement note rather than deleted outright: WHY a guard was withdrawn is the
part a future reader needs, and it is the only place that records that the
denylist never existed.
The reusable workflow and its selftest stay in this repo, dormant with no
callers, so restoring is three steps rather than a rebuild.
…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.
@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
LukasWodkaand others added 2 commits August 10, 2026 09:26
…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>
@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
LukasWodkaand others added 9 commits August 10, 2026 10:14
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)
…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.
…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 3 potential issues.

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 d09d208. Configure here.

Comment threadCLAUDE.md
Comment thread.github/workflows/version-bump-gate.yml
Comment threadrepo-inventory.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 0f7a553 into mainAug 10, 2026
19 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