Skip to content

release-train: develop -> staging - #655

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

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

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Sep 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

High Risk
Changes release-time publishing, credential scoping, and multi-layer leak prevention; a guard or workflow bug could expose dev repo content or push to the wrong target.

Overview
Adds an automated mirror publish path so a separate public repo gets only the customer deliverable (README, LICENSE, top-level docs/*.md, and release assets)—not Go source, CI, or workflows.

New workflow mirror-publish.yml runs after the Release workflow succeeds (or via manual dispatch with dry-run). It plans what to ship (newest stable tag updates the mirror default branch; prereleases and older stables mirror releases only), fetches the release tag as read-only data at a pinned SHA, runs publish-guard.sh (allowlist, .publish-forbidden, private tenant needles, pinned gitleaks), then publish-mirror.sh pushes the tree and creates the mirror release using a GitHub App token scoped to MIRROR_REPO.

New policy and scripts: .publish-include / .publish-forbidden, scripts/publish-guard.sh, scripts/publish-mirror.sh, plus hermetic verify harnesses wired into the Installer (shell) job in build.yml. scripts/RELEASE_CHECKLIST.md documents the mirror step.

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

…verable mirror (#654)

* ci(mirror): fail-closed publish guard + publisher for the public deliverable mirror

The public face of this repo becomes a separate, deliverable-only mirror:
README/LICENSE/docs on its branch and each release with its signed assets.
No Go source ever leaves. Nothing reaches the mirror except through the new
Mirror publish workflow, and nothing leaves that workflow except what
scripts/publish-guard.sh staged from the explicit allowlist
(.publish-include) and cleared through four guards: forbidden paths and
forbidden strings (.publish-forbidden), and gitleaks. Every guard fails
closed; "could not tell" never publishes.

scripts/publish-mirror.sh does the push: it refuses an unset mirror and a
mirror equal to this repository, pushes plainly (never force), and never
overwrites an existing release. The workflow runs after Release completes
and on dispatch with dry-run defaulting to true; the mirror name
(MIRROR_REPO) has no default. Customer identifiers for the string scan are
supplied privately at publish time, not committed to this public file.
build.yml's Installer job shellchecks the scripts and runs both harnesses.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(mirror): split the string guard into refuse and report tiers

A guard that can never pass is a guard nobody arms: on the real tree the
single [strings] tier refused with 26 hits (README and the two installers), all of them internal ticket
identifiers and non-production hostnames whose fate is still a decision,
not a defect. The scan now has two tiers in .publish-forbidden:

- [strings-refuse]  mailboxes (support@ spared), arn:aws:, ECR account ids,
                    plus the private tenant needles from --extra-forbidden.
                    A hit refuses, as before.
- [strings-report]  ticket/RFC identifiers and non-production hosts. Hits are
                    counted and printed (per-needle totals, ten most-hit
                    files) but refuse only under --strict.

The list itself is refused (exit 2) on an unknown section header, a needle
in both tiers, or an empty [strings-refuse]. The header regex is loose on
purpose so a misspelt header is refused by name rather than read as a
needle of the section before it.

The workflow gains a `strict` dispatch input (default false) and honours the
PUBLISH_STRICT repository variable for every run, including workflow_run,
which has no inputs; flipping either arms the report tier.

Tests: 34 -> 43 (42 in CI, where the real-gitleaks case skips). Report-tier hit alone -> exit 0 with count and table; the
same under --strict -> exit 1 naming [strings-report (strict)]; refuse-tier
hit -> exit 1 naming [strings-refuse]; duplicate needle, unknown section
and empty refuse tier -> exit 2 with the offender named. The committed list
is exercised needle by needle with inputs written in the test, and the real
tree is now asserted clean (exit 0). Each new check was mutation-proved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(mirror): run the tooling from this commit, treat the tag as data, keep prereleases off the default branch

Review findings on the mirror-publish workflow, each with its test:

- Untrusted checkout. The job checked out the release tag and then ran
  scripts/publish-guard.sh and publish-mirror.sh from it while the job went
  on to mint an App token. Now the one actions/checkout has no ref (the
  workflow's own commit), and the release tag is fetched separately into a
  detached worktree under RUNNER_TEMP as data — staged and scanned, never
  executed. Before the fetch the plan step requires the release object's
  tag_name to equal the run's tag and takes the expected commit from
  workflow_run.head_sha (a dispatch asks the API); the fetched tag must
  resolve to exactly that commit or the run refuses. The guard reads the
  allowlist and forbidden list from the tooling checkout, whatever the tag
  carries.
- Prerelease overwrote the mirror's default branch. workflow_run always
  publishes, and the tree push never looked at PRERELEASE. The plan step now
  derives publish_tree=false for a prerelease and says why; the default
  branch push is gated on it; the release is still created, marked
  prerelease, pinned to the mirror's current default-branch head — an empty
  mirror is refused rather than given an RC as its first content.
- Captured output hid refusals. `target` and `tree` ran through `$(...)`,
  so under set -e their ::error:: lines never reached the log. Both now run
  directly and write their results (repo=/name=, result=/sha=) through a new
  --output FILE option, which the workflow points at $GITHUB_OUTPUT.
- The gitleaks download carries --tlsv1.2 like every other privileged fetch
  in this repository.

scripts/tests/mirror-publish-workflow-verify.sh executes the plan, src,
target and keep step bodies read out of the workflow itself (gh shimmed, the
tag fetch against a real bare repo) and pins the shape — no checkout ref,
the tree push gated, the release step not, no captured publisher, one pinned
tag fetch — with mutations asserted to change the document before they are
judged; build.yml runs it beside the other harnesses.
publish-mirror-verify.sh covers --output for target and tree, including that
a refusal writes nothing and annotates stdout. Each new check was
mutation-proved against the real workflow and script. The release checklist
notes the trust shape and the prerelease behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(mirror): pin the fixture origin's HEAD so the tag-fetch case runs on a fresh runner

The workflow harness's bare origin relied on init.defaultBranch: unset on the
runner, its HEAD pointed at a `master` nobody pushed, the clone had an unborn
HEAD, and `rev-parse HEAD` handed the src step the literal word HEAD as the
expected commit — the case failed with "cut at HEAD" instead of proving the
pinned fetch (Installer (shell) on the previous push). The bare HEAD is now
set to main explicitly and the commit is read from the seed repository;
reproduced locally with GIT_CONFIG_KEY_0=init.defaultBranch
GIT_CONFIG_VALUE_0=master before and after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(guard): strip [allow] tokens as whole words, name private needles by number only

Two review findings on scripts/publish-guard.sh (kept byte-identical with the
client repository's copy):

- The [allow] pass removed a token with an unanchored, case-sensitive sed
  replace before re-testing the needle. A mailbox that merely ENDS in the
  public support address (devsupport@…) lost the token, the mailbox rule no
  longer matched, and the internal address could ship; a differently-cased
  public address was refused. The strip now matches the token as a whole
  word — not the tail of a longer local part, not the head of a longer
  domain, a sentence-ending dot still a boundary — and case-insensitively,
  as the scan itself matches.
- Refuse-tier hits printed the needle, and the workflow tees that log into
  the run summary. The needles from --extra-forbidden are the identifiers
  kept out of the committed list because this repository is public, so one
  deliverable hit would have published them in the log. Private needles are
  now scanned in their own pass and named `private needle #N` in every line
  the guard prints or writes (stdout, the report file, grep-error text); the
  committed needles are still named by pattern.

publish-guard-verify.sh: devsupport@ is refused, Support@Tracebloc.io. at a
sentence end passes, and the tenant case asserts the redacted name and that
the pattern appears neither in the output nor in publish-guard-report.txt.
Each check was mutation-proved by restoring the old strip and the old label
in the script and watching only its named cases redden.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(mirror): push the tree only for the newest stable release

Any non-prerelease tag set publish_tree=true, so a Release re-run or a
dispatch of an OLDER stable tag replaced the mirror's default branch with
that tag's README and docs — and, with the release already mirrored,
refused right after, leaving the rollback in place and paired with nothing.

The plan step now asks GitHub for the source repo's newest stable release
(releases/latest) whenever the tag is not a prerelease. A tag that is not
that release mirrors only its GitHub release, pinned like a prerelease to
the default branch's current head, and the log says why. If the newest
release cannot be read the run refuses rather than guess. The keep step's
name and messages cover both cases now.

mirror-publish-workflow-verify.sh: the gh shim answers releases/latest
from GH_LATEST_TAG / GH_LATEST_RC; new cases for an older stable tag
(publish_tree=false), an unreadable newest release (refused), a prerelease
not consulting it, and a plan-body mutation that disarms the comparison —
the older-tag case catches it. 20 cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(mirror): refuse a non-boolean isPrerelease; keep the guard summary under errexit

Two hardenings in the plan and guard steps of mirror-publish.yml:

- isPrerelease from the release API must be exactly `true` or `false`.
  A missing or malformed value (`jq -r` prints `null`) used to fall
  through into the stable path and arm the tree push; it is now refused
  before the newest-release question is asked.
- The guard step has a stable id (guard-tree) and catches the guard's
  exit status with `rc=0; ... | tee ... || rc=$?`. Actions runs the body
  under `bash -e`, so the earlier `rc=${PIPESTATUS[0]}` never ran on a
  refusal and the step summary stayed empty; the step still exits with
  the guard's own status.

mirror-publish-workflow-verify.sh runs every step body under `bash -e`
(as Actions does) and pins both: the null case, the refusal-to-summary
case, and a mutation for each (accept any isPrerelease -> null publishes
the tree; drop `|| rc=$?` -> summary empty) that the new cases catch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@tracebloc-release-train tracebloc-release-train Bot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Sep 11, 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.

✅ 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 d5f36e0. Configure here.

@tracebloc-release-train tracebloc-release-train Bot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Sep 11, 2026
@tracebloc-release-train
tracebloc-release-train Bot merged commit ebdfcc4 into staging Sep 11, 2026
53 checks passed
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 11, 2026 08:00
@LukasWodka

Copy link
Copy Markdown
Contributor Author

Functional review — staging batch of 2026-09-11 (07:25 UTC hop, run 34574301544) — PASS (FR reviewer: LukasWodka; self-signoff per RFC-BACKEND-1405 D6)

Decision: PASS. All 30 cards at FR on staging — design-system-v2 5, frontend-app 4, client-runtime 4, client 4, e2e-test-agent 3, tracebloc-py-package 2, start-training 2, cli 2, backend 2, .github 2 — advance to Ready for prod. Evidence pulled 08:26–08:50 UTC against the staging tips (backend 4b5f8601, client 69cbf8e9, client-runtime 36bb929f, design-system-v2 b9d7b8d7, e2e-test-agent 3a7f6b71, frontend-app f86ee508, start-training cd5c74d6, tracebloc-py-package ffb7af47, cli ebdfcc4, .github 008117d0).

Journey on the staging chart (governs). Three tier-a baselines with chart_ref=v1.9.115-rc.1 — the rc staging carries, including the ghcr.io training-image host (client#1041 + client-runtime#555) and the TRACEBLOC_DDP chart default (client#1045):

Channels. fr-assist run 34579205852 (08:26 UTC): all OK, 1 SKIPPED (credentials).

Per-repo staging deploys. Green on every tip: backend Tests / docker-build / Lint / Migrations check; client E2E auth-proxy / E2E mysql / CodeQL / Release Helm Chart (rc.1 published); client-runtime Publish images / Tests; design-system-v2 Tests; frontend-app Tests + smoke on staging https://github.com/tracebloc/frontend-app/actions/runs/34579505460 green; tracebloc-py-package Test Suite + TestPyPI rc publish; start-training CodeQL / Template rules; cli CodeQL; .github Selftests / CodeQL. The org-config caller switches (backend#3663, client-runtime#558, tracebloc-py-package#516): every kanban/gate workflow in those repos ran green after the merge.

One finding, filed, not this batch. e2e-test-agent Unit tests is red on the staging tip AND on develop's newest push: test_od_probes … od_golden_agrees_with_producers compares the committed OD golden against producer repos read live, and the engine's constants moved when tracebloc-engine reached main at 07:01 UTC. Detector working as designed; golden refresh tracked as e2e-test-agent#615. It blocks the next e2e mirror, not these cards.

Not verifiable on this battery. client-runtime#553 (one GPU unless the effective batch would still be the batch) is GPU-scheduling logic; the CPU k3d journey cannot exercise it. Interactive/TTY behaviour: none in the batch. Per-ticket behaviour beyond the journey's path, and steps a run skipped, remain unclaimed (README → Functional review).

@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-staging branch September 11, 2026 09:24
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 11, 2026 09:25
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