release-train: develop -> staging - #145
Merged
Merged
Conversation
… to SIGPIPE The gate whose only job is keeping customer names out of public repos was green on six public repos while having evaluated nothing. Three independent fail-open paths (backend#1409), all now refusals. 1. An unset/empty PII_DENYLIST warned and exited 0. The secret does not exist org-wide, so `pii-gate / pii-check` had never compared a single term anywhere. It now exits 1: a guard that cannot check must refuse, not pass. Same message covers a caller that forgot `secrets: inherit`. 2. `printf '%s' "$HAYSTACK" | grep -iqF` discarded matches. `grep -q` exits on its first match and closes the pipe, printf takes SIGPIPE, and under `pipefail` the pipeline returns 141 — so `if` read false and the hit was thrown away. Reproduced on the runner toolchain (bash 5.2.21, GNU grep 3.11): a denylisted term in the PR title with a 200KB haystack gave `rc=141 PIPESTATUS=141 0` and the old script printed "PII gate passed". Now a herestring: no pipe, nothing to break. grep exiting >1 is an operational error and is treated as "did not check", never as "clean". 3. `2>/dev/null || true` on the Compare API turned a 403, a rate limit and a jq error into an empty commit list, and the gate passed on title+body alone. The response is now read into a file, failures are reported with gh's own stderr, and `commits[] < total_commits` (the API's 250 cap) is refused as a truncated haystack rather than scanned and called clean. Three smaller fail-opens found while walking the same error paths: a denylist of only commas passed every PR having compared nothing; the override test matched any label *containing* `pii-gate-override`, so a label like `discuss-pii-gate-override-policy` disarmed the gate; and unquoted word splitting glob-expanded any term containing `*`. The pass message now names how many terms and commits were actually compared, so a green check is distinguishable from a green check that did nothing. Still open, needs org-owner rights (not this PR): create the PII_DENYLIST org secret, make `pii-gate / pii-check` a required check on the six public repos, and add the caller to public `.github`. Refs tracebloc/backend#1409 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hing" Two fail-opens in the previous commit, same class as the ones it fixed. The haystack is staged in a named temp file instead of a `<<<"$HAYSTACK"` herestring. Both avoid the pipe that was discarding matches, but if bash cannot create the herestring temp file the redirection fails, grep never runs, and the status is 1 — the one code that means "no match". Reading a named file, grep returns 2 on any read failure, so a check that did not happen cannot be mistaken for a clean one. It is also written once rather than once per denylist term. The term trim now checks sed exit status. A failed trim yielded an empty $t, which the empty-term guard on the next line skipped silently, so the term went unevaluated while the gate still reported a pass. Refs tracebloc/backend#1409 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… along
Bugbot on .github#130, High. The term loop ran a bare grep and then read $?,
but Actions invokes `run:` steps as `bash -e {0}`, and `set -uo pipefail` does
not clear that -e. grep returns 1 for "no match", which is the ORDINARY result
here, so errexit terminated the step before rc was ever assigned. The 0/1/*
case could only ever see a match.
The direction is fail-closed, so nothing leaked — but the gate blocked EVERY
clean PR across all six public repos, aborting on the first denylist term that
happened to be absent. It only looked correct while the secret was unset, which
is exactly the state this PR exists to fix: the moment PII_DENYLIST is populated,
the gate would have gone red on everything.
grep is now the left operand of `||`, which exempts it from errexit while still
delivering the status to rc, so 1 ("checked, found nothing") and 2 ("could not
check") stay distinguishable — the whole point of backend#1409.
Also spells out `set -euo pipefail`. errexit was already on; writing it down
stops the next reader inferring it was off, which is how this was written.
Verified by extracting the real loop and running it under `bash -e`:
clean PR, 3 absent terms -> CHECKED=3 HIT=0, completes (was: exit 1)
term present, first slot -> CHECKED=2 HIT=1
term present, last slot -> CHECKED=3 HIT=1
empty/whitespace term -> skipped, CHECKED=2
unreadable haystack -> rc=2, fails closed with the crafted message
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(pii-gate): fail closed when unconfigured, and stop losing matches to SIGPIPE (backend#1409)
Paired with release-train#25, which enrols them. repos.yml and this inventory are two files in two repos that must agree, and the caller-drift audit compares them -- so neither change is complete alone. MERGE ORDER MATTERS, and this PR is red until its pair lands. Verified locally: with this edit and release-train/main unchanged, the audit reports exactly model-zoo: repos.yml says on-train=False, the inventory says True start-training: repos.yml says on-train=False, the inventory says True That is correct behaviour, not a defect in this change. Merge release-train#25 FIRST, then re-run `audit` here and merge this. The reverse order is equally red, just on the other side. Both repos already have an fr-gate caller on develop and on their prod branch, so the finding's stock warning about an ungated hop does not apply -- the same distinction as .github#132 this morning. Worth noting as a limitation rather than working around it: the guard has no way to express "coordinated two-file change in flight". It compares live state to the checkout, so any cross-repo invariant is red for the window between the two merges. A follow-up could let an inventory entry carry a pending-PR reference, but silence during that window would be worse than noise. Refs #1420
…caller The drift guard caught my own change. model-zoo#115 and start-training#32 added code-quality callers this morning; their inventory entries still said `exempt: *code_quality_caller_missing`, so `audit` reported model-zoo: code-quality.yml is marked `exempt` but a caller exists start-training: code-quality.yml is marked `exempt` but a caller exists Both flipped to `required # code-quality-caller.yml`. Audit now clean: 20 of 20 read, no drift. Selftest 42/42. Third instance today of the same two-file coupling -- adding a caller, or enrolling a repo, without updating the inventory that asserts the fact. The earlier two were release_train booleans (.github#132, #135); this one is a caller-vs-exemption pair. The shape is identical: a fact recorded in two repos where only one got edited. Also corrected the shared anchor's text, which still implied all nine original citations stood. Down to three -- claude-skills, docs, release-train -- and the anchor now names them plus what remediated the other two, so the next reader does not have to re-derive the count from grep. Refs #1420, #1415
.github is enrolling in the release train (D1), and the train waits on `gate / gate` before a staging -> prod hop. This repo had no fr-gate caller at all, so there was no such check to wait on -- #1276 correctly refused to make `gate / gate` required here for that reason. Pinned @main like every other caller. Open question 3 is answered in favour of branch promotion: one standard process for every repo, because a second promotion model is how drift starts. Names the consequence rather than leaving it to be discovered: this repo hosts the reusable, so a change to fr-gate.yml on develop is not gating its own PR -- main's version runs. That is exactly why the develop -> staging -> main path matters more here than anywhere else. `staging` is the only place a new gate runs against real board state before sixteen repos consume it at @main. `branches:` omits `master`: this repo has never had one, and listing a branch that does not exist is how dead filters accumulate (backend#1428). Refs #1420, #1276
Paired with the repos.yml entry. .github now has everything the train needs: staging branch created from main @ ee105ce, so its ancestry already contains prod's -- no reconciliation on the first hop staging protection 1 review, dismiss-stale, actionlint required, conversation resolution, no force-push fr-gate caller this PR -- there was none, which is why #1276 could not make gate / gate required here App bypass tracebloc-release-train on staging AND main, verified by read-back (that write returns 200 and silently drops the value when the App is not installed) merge-commit-only ruleset on main + staging; develop untouched Open question 3 answered in favour of branch promotion, so .github runs the same develop -> staging -> main path as every other enrolled repo. One process, because a second promotion model for one repo is how drift starts. Refs #1420, #1405
Bugbot is right, and the problem is my sequencing, not the flip. I bundled the inventory flip here to avoid opening a fourth PR. But caller-drift compares live repos.yml against the checked-out inventory, so asserting release_train: true while release-train#26 is unmerged makes this PR fail its own validation -- and #26 is ordered AFTER #137, because the fr-gate caller has to reach develop first or settle waits on a check that never appears. That is circular: #137 cannot be green until #26 merges, and #26 should not merge until #137 has. One red window between the two files is unavoidable, but it belongs on develop where it is visible and expected, not inside a PR that cannot go green. So this PR is now the caller only. The flip follows #26. Refs #1420
…ns-stale fix(inventory): model-zoo and start-training now have a code-quality caller
ci(#1420): add the fr-gate caller so .github can join the train
The follow-up the file's own note prescribes: "Adding a caller and flipping its entry to `required` in the same PR therefore fails, because the caller is not on develop yet. Land the caller first, flip the entry after." .github#137 landed the caller and release-train#26 enrolled the repo, so both facts are now true and this records them: release_train false -> true fr-gate.yml exempt: *no_staging_branch_no_hop_to_gate -> required That exemption was accurate until today -- .github genuinely had no staging branch, so there was no hop to gate. It has one now (created from main @ ee105ce so its ancestry already contains prod's), and RFC-BACKEND-1405 open question 3 is answered in favour of branch promotion, which puts this repo on the same develop -> staging -> main path as everything else. Audit with this change: 2 findings, both rfcs entries that .github#138 fixes. Without it: 4. Selftest 42/42. Refs #1420, #1405
feat(inventory): .github is on the train and has an fr-gate caller
…ow has (#138) * feat(inventory): rfcs is on the board — record the two workflows it now has Paired with tracebloc/rfcs' board-visibility PR, which adds add-to-kanban.yml (byte-identical copy, blob 45aa70a) and a set-pr-status caller so RFC PRs get a card and land in `Code review`. Seven RFCs are open, none merged, none with a human review. They stalled because the board is how this team works and they were not on it -- rfcs had no .github/workflows directory at all. Also corrected every remaining exemption reason in this block. All nine said "rfcs was created 2026-08-01 and has no .github/workflows directory yet", which this change makes false. The seven still-absent callers are now recorded as STRUCTURAL rather than pending: rfcs publishes no artifact, has no deploy stages and is main-only by decision, so there is no hop for advance-deploy-env, fr-gate, fr-pass-comment or kanban-closure-router to act on. Written down as a decision, not carried as a debt that nobody intends to pay. MERGE ORDER: the rfcs PR first, then this. The audit compares live caller state against the checked-out inventory, so marking these `required` before the files exist reports them MISSING -- correctly. Note two of the four findings currently reported by this branch belong to .github#136 (model-zoo / start-training code-quality exemptions), which is also open and must land first. Three .github PRs now touch the same invariant. Refs #1405, #1415 * fix(inventory): per-caller reasons for rfcs, not one blanket text Bugbot on .github#138, and it is right. My first version asserted the same "no deploy hop to act on" reason for all nine callers. That is only true for some, and it silently overwrote reasons that were already accurate. Corrected, using the existing shared anchors where they apply: code-quality -> *code_quality_caller_missing (a REAL gap: gitleaks and house-rules would do work on markdown) kanban-closure-router -> *kanban_closure_router_caller_missing (a REAL gap: it acts on close events, which this repo has now that add-to-kanban creates cards) customer-priority-bump -> *customer_priority_bump_caller_missing public-pii-gate -> *private_repo_no_public_exposure wip-limit-check -> *wip_limit_check_has_no_callers fr-gate -> *no_staging_branch_no_hop_to_gate Only three keep a bespoke structural reason, and each says why it is distinct: advance-deploy-env (no `develop`, so no dev stage to advance into), fr-pass-comment (no `staging`, so the column it drains is unreachable -- which is what separates it from the repos that HAVE the stage and lack the valve), and the stale-backlog copy (an untouched RFC is a decision nobody took, not a stale backlog item). The general lesson, which is why Bugbot's catch matters more than the diff: an exemption whose stated reason does not describe the caller is worse than no exemption. It reads as decided when it is unexamined, and #1415's whole premise is that the reason is the finding. Selftest 42/42. Refs #1405, #1415
.github hosts the public-pii-gate reusable but ran no PII gate on its own PRs — the only public repo with that gap (D1 enrolment, backend#1420). Self-reference pinned @main, matching fr-gate-caller.yml. Co-authored-by: Claude <noreply@anthropic.com>
…y (backend#1420) (#142) Reflect the fleet-wide convention: callers now pass secrets: inherit even though this reusable uses no secrets. Clarifies it's a no-op bounded by the permissions block, adopted to clear a recurring Cursor Bugbot false positive. Co-authored-by: Claude <noreply@anthropic.com>
…nded (backend#1420) (#143) The caller-drift audit failed on the `.github` develop -> staging hop (#140) with two findings, both real: - .github: public-pii-gate.yml marked `exempt` but public-pii-gate-caller.yml exists - docs: code-quality.yml marked `exempt` but code-quality-caller.yml exists Both callers were added TODAY under backend#1420 (.github#141, and docs' code-quality caller) and neither entry was flipped. This is the same two-file coupling as repos.yml vs release_train, and the third time this session it has been the guard rather than a human that noticed. Flipped both to `required` with per-caller reasons rather than a bare value, and recorded what each gate can actually do: - docs' code-quality is that repo's ONLY content gate (off-train, python:false / shell:false measured), so gitleaks + house-rules are the real checks; soft-fail stays advisory until a first green run. - .github's PII gate is PRESENCE, NOT TEETH: the reusable is unarmed org-wide (PII_DENYLIST unset, backend#1409, now parked in the backlog), so it runs green without inspecting anything. `required` asserts the caller exists -- what this file tracks -- not that the gate can fail. Saying so here beats a future reader inferring teeth from the word `required`. Also corrected the code_quality_caller_missing anchor: it claimed THREE citations and named claude-skills, docs and release-train while FOUR entries aliased it -- `rfcs` was added later in the same session and never listed. Now three for real (claude-skills, release-train, rfcs), with a note to trust grep over the sentence. Verified: PyYAML parse OK (20 repos), selftest 42/42, and the real audit run against live org state reports "No drift. Every repo read, every entry matched." (20 of 20).
…rolment turned stale (backend#1420) (#144) release_train: false -> true (release-train#30 landed first, per the two-file coupling this guard audits). advance-deploy-env: the caller existed on MAIN only (docs#70, a docs-exception direct merge) where develop-first audits cannot see it; docs#80's backmerge fixed that. fr-gate: staging now exists and the train polls 'gate / gate' fail-closed, so docs#80 added the standard caller — the 2026-06-04 docs exception is NOT revoked, it lives beside the train path. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0d92fe6. Configure here.
LukasWodka
added a commit
that referenced
this pull request
Aug 4, 2026
…sents RED, not green (backend#1409) Bugbot caught the contradiction on the staging hop (#145): d248583 flipped the reusable to fail closed when PII_DENYLIST is unset, while this note still said 'runs green without inspecting anything'. Record how unarmed presents now, and that the red blocks nothing: pii-check is a required status check nowhere (all 7 public repos x all branches, measured 2026-08-04). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sents RED, not green (backend#1409) (#146) Bugbot caught the contradiction on the staging hop (#145): d248583 flipped the reusable to fail closed when PII_DENYLIST is unset, while this note still said 'runs green without inspecting anything'. Record how unarmed presents now, and that the red blocks nothing: pii-check is a required status check nowhere (all 7 public repos x all branches, measured 2026-08-04). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed
release-train/to-stagingbranch (a mirror ofdevelop), so it never collides with a human PR. Merged only when the fr-gate is green.Recreated from #140 (app-authored, predating the RELEASE_TRAIN_PR_TOKEN convention — Bugbot does not review app-authored PRs, so it could not pass settle). Mirror unchanged; audit and gate were already green on this exact head. This is .github's first-ever develop → staging hop (backend#1420, Q3's staging window for the 118 @main callers).
Note
Medium Risk
PII gate behavior change will turn checks red org-wide once promoted to
mainwhilePII_DENYLISTremains unset; inventory edits affect caller-drift enforcement across the fleet but do not change application runtime code.Overview
This promotion hardens the public PII reusable so it fails closed when the scan cannot complete: missing
PII_DENYLIST, unreadable Compare API responses, truncated commit lists (>250), haystack staging failures, empty denylist after trim, andgrepoperational errors all exit non-zero instead of warning-and-passing. It also fixes bash/grep reliability (set -euo pipefail, haystack in a temp file to avoid SIGPIPE/pipefailfalse negatives,grep || rc=$?under errexit) and tightens override to an exactpii-gate-overridelabel viajq..githubgains local callers for workflows it hosts:fr-gate-caller.yml(PRs tostaging/main) andpublic-pii-gate-caller.yml(all PRs), both pinned@mainwithsecrets: inherit. Code-quality adoption docs now recommendsecrets: inheritfor fleet consistency despite no secrets in that reusable.repo-inventory.ymlis updated to match remediated fleet state:.githubon the release train with required FR and PII callers; docs, model-zoo, and start-training enrolled / code-quality and related callers markedrequired; rfcs exemptions split per caller with accurate reasons plusset-pr-statusandadd-to-kanbanrequired.Reviewed by Cursor Bugbot for commit 64bbbc3. Bugbot is set up for automated code reviews on this repo. Configure here.