Skip to content

release-train: develop -> staging - #145

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

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

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Aug 4, 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.

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 main while PII_DENYLIST remains 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, and grep operational 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/pipefail false negatives, grep || rc=$? under errexit) and tightens override to an exact pii-gate-override label via jq.

.github gains local callers for workflows it hosts: fr-gate-caller.yml (PRs to staging/main) and public-pii-gate-caller.yml (all PRs), both pinned @main with secrets: inherit. Code-quality adoption docs now recommend secrets: inherit for fleet consistency despite no secrets in that reusable.

repo-inventory.yml is updated to match remediated fleet state: .github on the release train with required FR and PII callers; docs, model-zoo, and start-training enrolled / code-quality and related callers marked required; rfcs exemptions split per caller with accurate reasons plus set-pr-status and add-to-kanban required.

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

LukasWodka and others added 18 commits August 3, 2026 09:05
… 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>
@tracebloc-release-train tracebloc-release-train Bot 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 4, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0d92fe6. Configure here.

Comment thread .github/workflows/public-pii-gate.yml
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>
@tracebloc-release-train tracebloc-release-train Bot 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 4, 2026
@tracebloc-release-train
tracebloc-release-train Bot merged commit 41f45bf into staging Aug 4, 2026
20 checks passed
Sign up for free to 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