Skip to content

release-train: develop -> staging - #218

Merged
tracebloc-release-train[bot] merged 1 commit into
stagingfrom
release-train/to-staging
Aug 11, 2026
Merged

release-train: develop -> staging#218
tracebloc-release-train[bot] merged 1 commit into
stagingfrom
release-train/to-staging

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 11, 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
Touches release-train promotion merge enforcement and supply-chain tag protection auditing; incorrect inventory or API read logic could miss real drift or block merges on false positives.

Overview
Adds a third conformance family alongside callers/copies and branch protection: GitHub rulesets that classic protection cannot express—merge-commit-only on promotion branches and the v* tag trust root on tag-publishing repos.

repo-inventory.yml gains fleet-wide ruleset_policy (match by target + rules, not ruleset name) and per-repo rulesets cells (required or written exempt) for all repos.

scripts/caller-drift.py loads and validates that contract, fetches each repo ruleset via listing + per-id detail (for bypass actors and merge methods), classifies rulesets by shape, and flags missing/weakened rulesets, stale exemptions, wrong merge methods, and exact bypass-allowlist drift. Ruleset API failures use a separate unreadable bucket so they do not abort caller findings or masquerade as branch-protection failures.

scripts/tests/caller-drift-selftest.py adds offline coverage for the main ruleset failure modes and schema gates.

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

…d (backend#1681) (#212)
* feat(inventory): audit rulesets — the protection layer nothing checked (backend#1681)
GitHub runs two protection systems. `read_protection()` reads the second one, but
only to satisfy the seven classic-expressible properties -- so the guard can tell
that a branch is protected, never that a ruleset EXISTS, what it permits, or who
may bypass it. Grepping the org's 26 workflows for a ruleset assertion returns
nothing.
Two things rest entirely on that unaudited layer, and both were drifting:
* `allowed_merge_methods: ["merge"]` on every promotion branch -- not
expressible in classic protection AT ALL, and the only thing stopping a
promotion PR being squash-merged, which collapses the merge-commit ancestry
promote-repo.sh's squash guard reads and jams the next prepare.
start-training carried NO ruleset whatsoever. Its classic protection was
fully compliant, so the existing guard reported it conformant.
* the `v*` tag trust root. `release-python` -- four engineers -- held `always`
bypass on backend, data-ingestors and tracebloc-py-package, two of which
publish ON the tag (PyPI, GHCR). Nothing could see it; it was found by hand.
This adds a `ruleset_policy` of two kinds and a per-repo `rulesets` cell:
* READS `/rulesets/{id}` per ruleset, not the cheaper endpoints. Load-bearing:
`/rules/branches/{b}` -- the one read_protection() already calls -- omits
`bypass_actors` ENTIRELY, so asserting an allowlist from it would assert
nothing and report a pass.
* CLASSIFIES on target + rules, NEVER on name. client's tag ruleset is named
"R8 trust root - protect v* release tags" while its five peers use
"Protect v* release tags (supply-chain trust root)"; a name-keyed check
reports client as missing its trust root.
* BYPASS IS AN EXACT ALLOWLIST, not a floor -- an unexpected actor is the
finding, which is what turns a future release-python-style grant into a
build failure instead of something an audit has to stumble on.
* also asserts enforcement==active (an `evaluate` ruleset looks protective and
enforces nothing) and the required rule types.
No `divergent` state here, deliberately: a ruleset that exists but permits a
squash merge, or carries an extra bypass actor, is drift -- not a documented
variant -- because that is precisely what the property exists to catch.
Verified, not asserted:
* selftest 97 -> 111 pass, 0 fail. The new cases assert that a MISSING
ruleset, a squash-PERMITTING one, an `evaluate`-only one, an EXTRA bypass
actor and a STALE exemption are each findings, that a differently-named tag
ruleset still matches, and that an unreadable read is UNREADABLE rather than
a silent pass.
* One of those tests caught a fail-open I had just written: an early return
skipping the read when every cell was `exempt`. It saves an API call and
makes "this repo has no rulesets" unfalsifiable -- and a fully-exempt repo
is exactly where an unnoticed ruleset would sit forever. Removed, with the
reasoning recorded so it is not re-added as an optimisation.
* LIVE against all 20 repos: "No drift. Every repo read, every entry matched."
* MUTATION: the same run with the tag bypass allowlist pointed at a team
nobody holds produces 6 findings -- one per tag-ruleset repo, each naming
the unexpected and the missing actor. Green means something because red is
reachable.
The report line now names the ruleset kinds too; it previously understated its
own coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(caller-drift): ruff E702, and stop calling a ruleset outage a protection one (.github#212)
Two review findings on this PR:
* `quality / ruff` failed E702 (two statements on one line) in a selftest I added.
Fixed; `ruff --isolated --select E4,E7,E9,F scripts/` now passes, which is the
org gate's exact selection.
* Bugbot (Medium): `evaluate_rulesets` appends its read failures into
`protection_unreadable`, but the summary and the `::error::` line both said
"branch-protection read(s) FAILED ... protection state UNKNOWN". A
rulesets-only outage therefore named the wrong control plane and claimed
protection was unknown when it had been read successfully. That is exactly the
honesty this file exists to enforce, so: wording is now "protection/ruleset",
and the bucket carries a comment saying what it holds and why the two share it
(both mean "the caller/copy audit for this repo SUCCEEDED, a different control
plane did not").
Selftest re-run: 111 pass, 0 fail.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(caller-drift): separate ruleset read-failures from protection; includes_parents=false
Bugbot (.github#212):
- read_rulesets listed rulesets WITHOUT includes_parents=false, so org/enterprise
rulesets folded in and their ids 404'd on the per-ruleset get-by-id, failing the
whole audit. Scope the listing to this repo's own rulesets.
- ruleset read-failures were funnelled into protection_unreadable, so a rulesets-only
outage reported 'protection state UNKNOWN' (and vice versa). Give rulesets their own
unreadable bucket and name each layer separately in the summary + ::error::.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(caller-drift): stubs match /rulesets?includes_parents=false (Bugbot #212)
The listing stubs matched endswith('/rulesets'); read_rulesets now appends the
query string, so strip it before the suffix check. Restores the conforming /
missing / weakened ruleset selftest cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
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.

✅ 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 7716f00. 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 11, 2026
@tracebloc-release-train
tracebloc-release-trainBot merged commit a3f0f53 into stagingAug 11, 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