Skip to content

release-train: staging -> main - #234

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

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

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 12, 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

Medium Risk
New org-wide reusable workflow contract and inventory changes affect every repo once callers land; fixes touch fleet conformance auditing and shared code-quality behavior at scale.

Overview
Adds a blocked merge gate (reusable blocked-gate.yml + blocked-marker.py) that fails PRs with the org blocked label or title markers like [blocked on …], DO NOT MERGE, or HOLD until …, with a path-filtered selftest biased toward false positives. The reusable is registered in repo-inventory.yml but exempt fleet-wide until a follow-up wires callers (backend#1729).

Conformance and quality fixes:caller-drift.py now treats ruleset-only branches as protected when probing stale exempt entries (backend#1681) and prints a per-repo conformance matrix; gitleaks summary generation avoids jq | head SIGPIPE so soft-fail: true cannot hard-fail on large finding sets. Fleet pin:add-to-kanban moves to actions/add-to-project@v2.0.0; cli’s kanban copy is no longer marked divergent for that pin.

Docs/process: new .cursor/BUGBOT.md, org “fix the class” guidance, PR template Sibling check item, README pin note.

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

LukasWodkaand others added 12 commits August 11, 2026 23:08
Four defects in one day shared a root cause: a fix that addressed the instance in
front of the author and left the rest of its class behind.
1. a guard wrapped around load(checkout) while discover(root) stayed bare
2. an epoch-completeness helper used at two sites while a third did it inline
with a raw non-distinct .count()
3. a core.hooksPath guard that canonicalised the DIRNAME of the path, not the
path, so core.hooksPath=.. escapes it
4. a pre-push hook that probed for make but not for the ruff/pytest toolchain
it then calls
The rule names both shapes deliberately. 1 and 2 are other CALL SITES -- findable
by grepping what you changed. 3 and 4 are single-site: another INPUT reaches the
same guard, and no grep of the symbol will show you that. A rule that only says to
check the code nearby catches half the class, which is how 3 and 4 shipped.
Same week, from the other direction: a fleet-wide verification script accumulated
five defects of exactly this kind, and every one surfaced by RUNNING it across all
16 repos rather than by reading the diff (tracebloc/release-train#48). Reviewing a
predicate is not the same as enumerating what reaches it.
Canon only. The sync (backend#1602) will report the fleet DRIFTED and can open the
CLAUDE.md PRs; per-repo copies are never edited by hand.
Refs backend#1646
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…oth (backend#1681) (#222)
`evaluate_protection`'s exempt-staleness check was `if probe.classic_present:`.
A branch protected SOLELY by a ruleset returns 404 from the classic endpoint, so
it read as unprotected and its exemption stayed silently valid -- the guard would
never notice that a role it believes is out of scope is in fact a real, protected
branch.
That is the two-systems defect the 20-line header of read_protection() exists to
prevent, sitting in the one place that decides whether an exemption is still
true. `probe.rulesets` is populated by the very same call; it was simply never
consulted. The finding now also names WHICH layer covers the branch, so the
reader is not sent to re-derive it.
The gap survived because the coverage stopped one path short: `_ruleset_only`
already existed in the selftest and was only ever pointed at the `required` path,
never at `exempt`. It is now used for both.
Verified:
* selftest 111 -> 117 pass, 0 fail
* MUTATION: restoring the old `classic_present`-only condition makes the new
case FAIL (findings=[]), so the test genuinely covers the hole rather than
passing either way
* LIVE against all 20 repos: "No drift. Every repo read, every entry matched."
-- no exemption was actually hiding behind this, so closing it costs nothing
today and catches the next one.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…well as drift (backend#1608) (#223)
Increment 3. The audit has always known every repo's state per family and only
ever emitted the FAILURES, so a green run said "No drift" and nothing about what
was actually covered. You could not tell a fleet that conforms from a fleet that
was barely checked -- which is the same question this epic keeps finding the
wrong answer to elsewhere (a caller that is present but advisory, a required
check that never reports).
Adds a per-repo x per-family table to the report: callers, copies, protection,
rulesets, for all 20 repos, with the train flag alongside.
Cells are deliberately three-valued:
OK evaluated and matched
N that many findings
? that family could not be READ
`?` is the point. "Zero findings because we checked" and "zero findings because
we never looked" must not render the same, and the whole guard exists to refuse
that conflation -- so the screen refuses it too.
Counted by DELTA around each family's block rather than by parsing the finding
strings. Those strings are prose written for humans; keying a table off them
would break the first time one is reworded.
Verified:
* LIVE, all 20 repos: renders 20 x 4, every cell OK, exit 0
* MUTATION: with an impossible required-check in the develop baseline, the
protection column shows **1** for exactly the 16 train repos and OK for the
4 non-train ones whose develop role is exempt, exit 1. The screen shows red,
and shows the SHAPE of the failure -- one family, one cohort.
* selftest 116 -> 122, including that a clean row and an unread row do not
render identically, and that an unreadable FAMILY marks only its own column
* ruff --isolated --select E4,E7,E9,F -> All checks passed!
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…mplate check (backend#1680) (#224)
Four instances on 2026-08-11, in TWO shapes -- and a rule aimed only at
"neighbouring bugs" catches half of them:
other CALL SITES
* discover() left bare beside a wrapped load()
* an epoch helper used at 2 sites while a 3rd did it inline
other INPUTS TO THE SAME GUARD
* the hooksPath guard canonicalised dirname, not the path, so `..` escaped it
* the pre-push hook probed `make` but not the venv toolchain it calls
The canon line names both shapes. The PR-template line is the part that does the
work: it produces an artifact a reviewer can see is MISSING, where a prose rule
produces nothing to look for.
Why mechanical rather than remembered: the stuck-run detector built FROM today's
lessons reproduced the same mistake -- it encoded the symptom (runner=NONE)
instead of the condition (nothing progressing), and would have cancelled a
healthy production deploy. The pattern survives being named, which is the whole
argument for a checklist item over a paragraph.
Cost, stated rather than discovered: editing the canon drifts all 19 repos and
needs a sync fan-out. That is the third such edit this week and is exactly the
treadmill worth deciding about (auto-merge on green, or batching) -- tracked on
backend#1680.
Verified: standards-sync selftest 27 checks / 0 failed; the report still splices
the block cleanly against all 19 targets.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…s passed (backend#1680) (#225)
.github had no `.cursor/BUGBOT.md` — one of five repos without one — despite being
public and holding the reusables ~120 callers consume at @main, plus the conformance
contract itself.
Written from this repo's actual defect history rather than generic advice. Nearly
every real finding here is one shape: a guard that reports success it did not verify.
The guide names the variants seen in this repo (empty response read as 'nothing
found', `|| echo 0`, a grep in a pipefail pipeline returning 141, a required check
that is path-filtered so it can never report, a soft-fail default on a required
check), plus the two-protection-systems trap and the @main caller/callee ordering rule
that turns a mistimed input into startup_failure.
Also records the non-issues that were being re-raised: retired pii-gate contexts,
the deliberate `strict: false`, and the long incident comments that are load-bearing
rather than verbose.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#220 and #224 both landed the same rule one minute apart, so org-standards.md
carried two near-identical 'Fix the class, not the instance' bullets. Because
this file is the synced canon, the duplicate would propagate into every repo's
CLAUDE.md (Bugbot, on the develop->staging mirror #226, which it blocked).
Keeps the line-25 wording: the neighbouring bullets all use unbolded lead-ins,
and the removed one was the only bullet in the file starting with bold. It also
sat between the post-PR and recurring-finding bullets, splitting that flow.
Verified: one occurrence of the rule remains, zero bullets start with bold.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…pipe to head (#228)
`jq ... /tmp/gitleaks.json | head -50` builds the gitleaks findings table for
the step summary. The step runs `set -uo pipefail`, and Actions launches every
`run:` block as `bash -e {0}` — so errexit is ON despite the `set -uo` reading
as though it were off. `head` closes the pipe after row 50, jq takes SIGPIPE
once the output no longer fits the pipe buffer, pipefail turns that into a
pipeline status of 141, and errexit aborts the step mid-summary.
The short table is the least of it. The abort happens before the
`::error::`/`::warning::` annotations are emitted and before the soft-fail
`exit 0`, so a repo configured `soft-fail: true` HARD-fails instead of
reporting — and the summary loses the "rotate it first" remediation guidance
entirely.
Capture the rows into a variable and slice the capture with a here-string, so
jq always runs to completion and the exit status is its own. The emit is
guarded on a non-empty capture, because `head <<<""` would print one blank
line where the pipeline printed nothing.
Latent, not live: it needs roughly 700+ findings to fill a 64K pipe buffer,
and gitleaks findings normally sit at zero. Measured on darwin it tips from
~250 findings (~24K of rows), nondeterministically right at the boundary
because it is a race.
Verified against the real step body extracted from this workflow: at 0/1/50/
51/200 findings old and new produce byte-identical summaries, the same exit
code and the same annotation count; at 1000 findings old returns 141 with 0
annotations, new returns 0 with all 1000. actionlint 1.7.12 (the version CI
pins) reports zero findings.
Refs: backend#1778
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* ci(gate): fail a PR that declares itself blocked (backend#1729)
data-ingestors#468 was titled "... (D10) [blocked on S2]". It collected three
approvals and was merged by its author 105 seconds after S2 merged. The author
waited for the blocker they had written down -- but "S2 merged" was never the
real precondition, and nothing could tell the difference. dev and staging
ingestion broke within hours and stayed broken for a day (backend#1752).
The dependency was recorded as prose, in a place nothing reads. This makes that
sentence a merge gate: a PR whose title or labels say it is blocked fails a
check. Two open PRs carry the shape today -- client#490 ("HOLD until v0.8.0
image") and client-runtime#192 ("DO NOT MERGE").
What it does not do: decide whether the blocker is genuinely resolved. #468's
author satisfied the sentence they wrote and the real precondition was still
unmet. The claim is only that clearing the marker is a deliberate act.
The matching is the entire risk surface, so it lives in a tested script rather
than a YAML heredoc. Precision over recall: `unblocked`, `unblocks`, `blocker`,
`threshold`, `holder` and `wipe` must not fire, or the gate gets called noise
and switched off -- the failure mode house-rules.sh names in its own notes.
No callers in this PR, deliberately. The reusable lands first so the matching
can be reviewed alone; repo-inventory.yml records it `exempt` everywhere with
the rollout named, because this file's own header forbids adding a caller and
flipping it to `required` in one PR. Arming is a briefing decision: two live
PRs go red the moment it is on, which is the gate working.
Verified: marker selftest 22 checks green, including the GITHUB_EVENT_PATH path
the workflow actually uses; both event-path assertions mutation-checked (drop
the title / drop the labels -> each fails). caller-drift selftest 116/0.
load_inventory + check_source_reusables pass on the edited inventory, and fail
as intended when the reusables row is removed. actionlint clean.
* fix(gate): anchor the WIP marker — measurement found a real false positive
Ran the matcher over 588 merged PR titles from ten tracebloc repos rather than
trusting the table I had imagined. It produced exactly one false positive:
chore(ci): retire the WIP-limit nudge
and it is the worst kind, because "WIP limit" is a domain term in THIS org --
wip-limit-check.yml is one of our own reusables, so the phrase recurs. The
(?<![a-z])/(?![a-z]) guards do not help: the character after "WIP" is a hyphen.
That is precisely the failure this gate cannot afford. A check that fires on a
normal chore PR gets called noise and switched off, and then catches nothing
ever again.
WIP-as-a-marker is a PREFIX convention (`WIP: ...`, `[WIP] ...`); WIP mid-title
is the noun. Anchoring to the start keeps every real marker and drops the false
positive. Over the same 588 titles the matcher now blocks exactly one:
data-ingestors#468 -- the PR this gate exists for.
Both new cases mutation-checked: un-anchoring `wip` fails 2 assertions.
* fix(blocked-gate): fail closed on a payload with no pull_request; minimal perms; label events (Bugbot #229)
- blocked-marker.py: a GITHUB_EVENT_PATH payload with no pull_request object now
refuses (exit 2) instead of returning an empty title -> 'not blocked' -> exit
0. A non-PR or malformed event was a silent fail-OPEN; it now fails closed like
the no-event-path branch. Selftest case added.
- blocked-gate.yml: drop 'pull-requests: read' — the script reads
GITHUB_EVENT_PATH, never the API, so the wider grant would exceed a minimal
contents:read caller and fail the reusable at startup.
- Document that callers must trigger on labeled/unlabeled too, or the label half
of the gate never re-runs until the next synchronize.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bumps this repo's own add-to-kanban caller, and flips repo-inventory.yml so
v2.0.0 is the fleet norm rather than the drift.
v1.0.2 declares `using: node20`. Node 20 is deprecated on GitHub-hosted
runners, so GitHub force-runs this action on Node 24 today and prints the
deprecation notice on every run -- including the 542 that succeeded. That
fallback is temporary; when it is withdrawn every repo still on v1.0.2 breaks
at once. v2.0.0 declares node24 and needs no fallback.
Not an outage fix: re-running the reported failed job unchanged on the v1.0.2
pin succeeded, so that failure was a transient TLS condition, not a runtime
incompatibility. cli and release-train have run this exact v2.0.0 pin
successfully (12/12 recent runs). v2.0.0 is input-compatible: it declares
project-url, github-token, labeled and label-operator; our callers pass the
first two.
Inventory changes:
* cli's add-to-kanban.yml goes divergent -> required. Its copy now matches
the canonical one byte-for-byte (blob 603751a), and leaving it
divergent would fail the guard with "marked divergent but now matches".
* release-train's entry stays divergent -- its header comment still differs
-- but the half of its reason claiming the v2.0.0 pin is drift is now
false, so the reason is corrected to name the header alone.
* stale-backlog.yml's divergent entries and devex-bootstrap's exemption are
untouched.
README.md told a new repo to pin v1.0.2, which would reintroduce the node20
pin this sweep removes.
This must merge AFTER the other 16 caller PRs: copies are compared by blob id
against the canonical file in the checkout, so until they land the audit
correctly reports 16 drifted copies. Documented awkwardness, fail-closed
direction, resolves itself.
Refs: tracebloc/backend#1816
Co-authored-by: Claude Opus 5 <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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f45fc9f. Configure here.

@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 12, 2026
@tracebloc-release-train
tracebloc-release-trainBot merged commit bd5b60d into mainAug 12, 2026
21 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