Skip to content

release-train: staging -> main - #662

Merged
tracebloc-release-train[bot] merged 7 commits into
mainfrom
release-train/to-main
Sep 11, 2026
Merged

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

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-main branch (a mirror of staging), so it never collides with a human PR. Merged only when the fr-gate is green.


Note

High Risk
New publish path moves release assets to a separate repo with secret-backed string scans and write tokens; login/session and env resolution changes affect every authenticated command.

Overview
Adds a fail-closed public mirror pipeline: after Release completes, mirror-publish.yml stages only allowlisted docs (.publish-include), runs publish-guard.sh (forbidden paths/strings, gitleaks, optional strict report-tier), and pushes via publish-mirror.sh to MIRROR_REPO using a scoped GitHub App token. Prereleases and non-latest stable tags mirror releases only without rolling back the mirror default branch; release tags are fetched as data and pinned to the release run SHA.

Ships backfill-releases.sh for one-shot historical release migration (dry-run default, BINARY_KEEP binary window, guard + SHA256 checks) and wires hermetic CI harnesses in build.yml for guard, publisher, workflow step bodies, and backfill (including mutation runs).

CLI (0.10.26): login reuses a backend-valid stored session (exit 0, no device flow) with --force to re-auth; fixes per-env profile lookup for mixed-case keys (Dev vs dev). RFC-0076: canonical $TRACEBLOC_ENV with legacy $CLIENT_ENV fallback in api.ResolveEnv, doctor cluster stage read, and updated help/golden strings. Shared classifyWhoAmIError for login and auth status --check.

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

LukasWodka and others added 7 commits September 11, 2026 07:54
…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>
…CEBLOC_ prefix (#657)

* feat(env): alias-first TRACEBLOC_ENV stage var, normalize toward RFC-0076

Normalize this repo's owned stage-selecting env var toward the RFC-0076
canon (backend#3391), alias-first — read new-or-old, never break an
existing deployment. TRACEBLOC_ENV is now the canonical name; the legacy
CLIENT_ENV is read as a fallback (remove_by: 2026-12-31).

- api.ResolveEnv: --env flag, then $TRACEBLOC_ENV, then legacy $CLIENT_ENV,
  then prod. Alias precedence lives in one place (stageFromEnv).
- doctor: the cluster's stage, read off the jobs-manager Deployment spec, is
  now read alias-first (stageFromClusterSpec) — a consumer-side alias so the
  edge chart can adopt the canonical key on its own S3-edge timeline.
- auth login / status --check help + the unknown-env error name the canonical
  var (legacy noted); goldens regenerated.
- env-resolution guard registers TRACEBLOC_ENV as a needle so a new read of
  either name lands in the allowlist; TestMain clears both stage vars so the
  higher-precedence canonical name can't make CLIENT_ENV-only isolation flaky.
- VERSION 0.10.24 -> 0.10.25 (version-bump-gate: env reads are packaged paths).

The other RFC-0076 config keys (registry REGISTRY_URL, telemetry, boolean
gates) have no owned, unprefixed occurrence here: TRACEBLOC_ALLOW_UNVERIFIED
already carries the prefix and no REGISTRY_URL/SKIP_TELEMETRY var exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): errcheck TestMain os.Unsetenv + bump VERSION 0.10.26 after develop merge (cli#656)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… flow (#651) (#658)

* fix(login): reuse a valid session instead of always starting a device flow (cli#651)

`tracebloc login` went straight to a device code even when the machine
already held a valid session for the target env. On a headless host that
is a dead end rather than an inconvenience: the credentials are on disk,
but the command insists on a browser approval it has no way to complete —
turning "re-run login to be safe" into a hard stop for any script or
runbook.

login now checks the session it already has before asking for one:

- A session for the target env that the backend accepts ends the command
  at exit 0 with "Already signed in as <account>". The session is
  confirmed with a live WhoAmI rather than trusted off disk — a revoked
  token is still a token on disk.
- `--force` is the opt-out (switching accounts, replacing a session
  believed stale) and skips the short-circuit entirely, including the
  probe. Same sense as `delete --force`: proceed despite the state that
  would otherwise stop you.
- Every fall-through says WHY first, and only claims what it can tell
  apart: a LOCAL expires_at that has passed is named to the second (and
  is not presented to the backend), a 401/403 is reported as rejected,
  and anything else — DNS, a 5xx — is "couldn't check", not a verdict on
  the session. A 426 surfaces the upgrade instruction and starts no flow,
  since a fresh flow would hit the same version floor.

"A session for this env" is resolved two ways, so credentials already on
disk aren't stranded: the current session when sessionEnv resolves it to
the target (the same predicate `auth status --check` uses, so login and
the installer's probe cannot disagree), else that env's own profile — a
machine on prod can hold a live dev token, and `login --env dev` adopts
it and switches current_env. The profile is written back under the key it
was found under, so a v1-migrated `"Dev"` config doesn't gain a second,
lower-cased profile beside the real one.

Tests cover each arm by asserting whether /device/code was requested at
all — the browser demand is the behaviour that matters. Goldens
regenerated for the new --force flag and copy.

Closes #651

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(release): bump VERSION to 0.10.26 for the login short-circuit (cli#651)

version-bump-gate failed on the previous head: VERSION still read 0.10.25,
v0.10.25 is already released, and this PR changes a published file
(internal/cli/auth.go, matching `internal/*`). The release train cuts the
tag from this file and never bumps it, so leaving it stale doesn't fail
here — it fails the next prod hop, days later, on somebody else
(backend#1561).

0.10.26 is free: v0.10.25 is the highest released final version and no
v0.10.26 tag exists. Patch, not minor — this ships one bug fix, and the
new `login --force` flag is additive with no change to any existing
invocation.

(Open PR #657 also touches VERSION, but bumps 0.10.24 -> 0.10.25, which is
already released — its gate is red for the same reason and it needs a
rebase. It does not claim 0.10.26.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(login): find raw-keyed profiles, and exit 130 on a cancelled probe (cli#651)

Two Bugbot findings on PR #658, both real.

1. storedSessionFor's second arm indexed cfg.Profiles with the ALREADY
   NORMALISED target env, so a live token written under a raw key such as
   `"Dev"` (config.migrateV1 stores a v1 `env` verbatim) went unseen the
   moment that profile stopped being the current one. `login --env dev`
   then ran a device flow and saved a SECOND profile under `"dev"`,
   stranding a perfectly good session beside it — on exactly the headless
   host this issue is about.

   The existing raw-key test could not see it: it keeps `"Dev"` CURRENT,
   which arm 1 catches before the map lookup is reached. The gap only
   opens after a `login --env` elsewhere has moved current_env.

   Lookup now folds the map's own KEYS (new profileKeyed). Exact match
   wins; the fold is a tie-break scanned in sorted order, so a config
   holding both `"Dev"` and `"dev"` cannot answer differently run to run
   on Go's randomised map iteration. The trim+lower-case is extracted
   from sessionEnv as normalizeEnv and shared, rather than hand-rolled a
   second time — a second copy is how the keys stop matching in the first
   place.

2. A cancelled context surfaces on the WhoAmI call as a plain error, so
   Ctrl-C during the new probe landed in the "couldn't check" arm: it
   printed "signing in again" and then failed RequestDeviceCode with exit
   1, where every other interrupt in login exits 130 silently. Guarded on
   ctx.Err() before the classification — the same guard, for the same
   reason, as pollForToken's.

Tests: the not-current raw-key case (asserts no flow, no duplicate
profile, current_env set to the FOUND key), a determinism test running
profileKeyed 50x over a config with three case variants, and a cancelled
probe asserting exit 130, silence, and zero device codes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(login): one shared WhoAmI classifier for both session probes (cli#651)

Review on PR #658: reuseStoredSession's 426 / 401-403 / everything-else
classification of a WhoAmI failure duplicated the identical block in
runAuthCheck a few dozen lines down. The duplication is one this PR
introduced, so it is fixed here rather than left for a follow-up —
"fix the class, not the instance".

classifyWhoAmIError returns a named whoAmIVerdict (whoAmIUnverified /
whoAmIRejected / whoAmIUpgradeRequired) plus the *api.UpgradeRequiredError,
so the caller surfaces the server's own version floor rather than a
paraphrase. The three arms are deliberately not collapsible: only
whoAmIRejected is a statement about the credential. A 5xx folded into it
would tell someone to re-authenticate during an outage, and a 426 folded
into it would send them to a browser step that cannot lift a version floor.

The COPY stays at the call sites. The two commands answer different
questions — "should I start a device flow?" vs "what is this exit code?" —
and say so in different words; only the classification is shared. No
user-facing string changed, and the goldens confirm it (regenerated, no
diff).

Test: a table over 401/403/426/500/404/429, a transport error, a cancelled
context, and wrapped 401/426 — wrapped because both call sites receive the
error through the api client's own fmt.Errorf wrapping, so matching the
concrete type alone would silently demote every real verdict to
"unverified".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…gs, newest 10 binaries) (#659)

* feat(scripts): one-shot release backfill to the public mirror (all tags, newest 10 binaries)

scripts/backfill-releases.sh carries this repository's historical releases
onto the public deliverable mirror once; mirror-publish.yml covers every
release cut after the mirror exists.

Decision: every published release gets its tag, its GitHub release and its
text assets (install.sh, install.ps1, SHA256SUMS, anything SHA256SUMS does
not list); binaries and their .sig/.cert only for the newest BINARY_KEEP
(default 10) releases. Mirror tags are annotated release markers on the
mirror's default-branch head carrying the original date and message, since
the mirror has no source commit to point at.

Reuses publish-mirror.sh `target` for the mirror-name rule and
publish-guard.sh for the string scan of every text asset and release body;
binaries are verified against the source SHA256SUMS before upload. Fails
closed (exit 2 on any incomplete read), refuses per release (exit 1),
idempotent (a re-run over a complete mirror writes nothing), resumable
(--from-tag / --only-tag). Dry-run is the default.

scripts/tests/backfill-releases-verify.sh: 30 offline assertions against a
recording fake gh, plus --mutations proving 6 anchored rules load-bearing.
Both wired into the Installer (shell) job. RELEASE_CHECKLIST.md gains §8.

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

* feat(scripts): backfill defaults to the workflow's fixed release notes; --notes source is the opt-in

Historical release bodies are GitHub's generated pull-request lists, and
nearly every one carries strings the publish guard's report tier counts.
The public mirror should not repeat them, so the backfill now writes the
same fixed notes mirror-publish.yml writes for new releases by default,
plus the original-date footer. `--notes source` still carries the source
body on explicit request and runs it through the guard as before.

Tests: the default path now expects fixed notes and no trace of the
source body; `--notes source` has its own positive test; the bad-body
refusal and the --strict report-tier refusal run under `--notes source`,
with a paired default-notes run proving the body is never staged. The
mutation list gains `notes-default-fixed` (flips the default back), which
reddens four tests.

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

* fix(scripts): die2 reports on stderr so a could-not-tell inside $(...) is seen, not swallowed

Nearly every jq_of call sits inside a "$(...)" assignment. die2 wrote its
::error:: reason to stdout, so inside the substitution the reason was captured
into the variable and never printed; the subshell's exit 2 ended only the
subshell, and the parent died under set -e with a bare status 2 the operator
could not name. The prerelease read sat inside a "[ ... ]" test in an && list,
where set -e is suspended, so that failure did not end the run at all - a
malformed release.json read as "stable".

- die2 writes to stderr: the reason reaches the operator from any depth, and
  the substitution's status 2 still aborts the assignment under set -e.
- the prerelease jq_of is hoisted into its own assignment before the test.
- harness: the fake gh gains FAKE_GH_GARBLE_RE (a call that "succeeds" with a
  non-JSON body); a new case pins that a garbled read parsed inside a
  substitution is exit 2 naming the file and filter with the reason in the
  output; a die2-stderr mutation (stdout die2) must redden it - proven: the
  mutant reddens exactly that case, 8 caught / 0 survived.

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

* fix(scripts): backfill marks a surviving stable release latest when the newest is refused; scratch commit is never signed

Two review findings, both confirmed against the script:

1. make_latest=true travelled only on the newest stable release's own POST,
   every older release being created with make_latest=false. When the newest
   stable was refused before that POST (SHA mismatch, guard hit, dangling tag)
   nothing on the mirror was marked latest and releases/latest answered 404
   until a human noticed the exit 1 and re-ran --only-tag. After the loop, an
   --apply run whose newest stable was refused AT CREATE TIME now PATCHes the
   newest stable release it did write to make_latest=true and says so; a
   refusal of a release the mirror already has leaves latest untouched, and
   a run that wrote no stable release warns and names the re-run. The
   happy path is unchanged (still 48 writes for 12 releases).

2. The guard's scratch checkout committed with only user.name/user.email
   overrides; a global commit.gpgsign=true on the operator's machine would try
   to sign as backfill@localhost, fail, and end the run before a release was
   planned. The scratch commit is never published: -c commit.gpgsign=false.

Harness: the fake gh now returns real release ids and serves PATCH
repos/<mirror>/releases/<id>; writes() counts PATCH; the sha-mismatch case
(which refuses v0.1.11, the newest stable) pins exactly one PATCH marking
v0.1.10 latest; a new case runs under GIT_CONFIG_GLOBAL with gpgsign=true
and a failing signer and demands a clean plan. Two mutations
(latest-fallback, scratch-commit-unsigned) each redden exactly their case:
37 passed, 10 caught / 0 survived.

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

* fix(scripts): the latest fallback sends make_latest as the string the API takes, and the fake gh refuses a typed one

The fallback PATCH added in the previous commit used -F make_latest=true,
which gh types as a JSON boolean. The releases API takes make_latest as a
string enum ("true"/"false"/"legacy") - the create path two lines away uses
-f for exactly that reason - so the PATCH would 422, die2 would fire, and
the one scenario the fallback exists for ended with the fallback failing
closed and releases/latest still 404ing. Now -f, matching the create path.

The harness let this through because the fake gh treated -f and -F alike.
It now records which fields arrived typed and answers a typed make_latest on
either release call with the API's 422, so the type is enforced where the
real endpoint enforces it. A latest-string-typed mutation (the -F line)
reddens the sha-mismatch and fallback cases: 37 passed, 11 caught / 0
survived.

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 01ec0c7. 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 c89c548 into main Sep 11, 2026
59 of 61 checks passed
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-main branch September 11, 2026 15:41
@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-main branch September 11, 2026 15:54
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-main branch September 11, 2026 15:54
@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-main branch September 11, 2026 17:17
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-main branch September 11, 2026 17:17
@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-main branch September 11, 2026 17:59
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-main branch September 11, 2026 17:59
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.

3 participants