Skip to content

release-train: develop -> staging - #661

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

release-train: develop -> staging#661
tracebloc-release-train[bot] merged 2 commits 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

Medium Risk
Login changes alter authentication and on-disk session selection (security-adjacent, headless/script paths); release backfill is operational tooling with strong harness coverage but writes to external mirrors when applied.

Overview
tracebloc login no longer always starts the device flow when credentials already exist for the target backend. Unless --force is passed, it looks up the stored profile (including case-folded env keys like "Dev"), skips locally expired tokens, probes the session with WhoAmI, and on success exits 0 with “Already signed in” while switching current_env to the reused profile key. Rejected, unreachable, or upgrade-required (426) outcomes are handled distinctly (shared classifyWhoAmIError with auth status --check); cancel during the probe exits 130.

Adds a one-shot scripts/backfill-releases.sh to copy pre-mirror GitHub releases onto the public deliverable mirror (dry-run by default, BINARY_KEEP for binaries, publish-guard + SHA256 checks, idempotent writes), plus scripts/tests/backfill-releases-verify.sh (including --mutations) and CI shellcheck / harness steps in build.yml. Release checklist documents the backfill workflow; login help/golden strings reflect the new behavior.

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

saadqbal and others added 2 commits September 11, 2026 15:54
… 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 ad3d919. Configure here.

@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 Sep 11, 2026
@tracebloc-release-train
tracebloc-release-train Bot merged commit 01ec0c7 into staging Sep 11, 2026
56 checks passed
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 11, 2026 12:28
@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-staging branch September 11, 2026 12:40
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 11, 2026 12:40
@LukasWodka

Copy link
Copy Markdown
Contributor Author

Functional review — staging batch of 2026-09-11 (93 cards, multiple hops today) — PASS at reviewer's explicit instruction (FR reviewer: LukasWodka; self-signoff per RFC-BACKEND-1405 D6)

Decision: PASS. All 93 cards at FR on staging — backend 17, design-system-v2 11, tracebloc-engine 9, e2e-test-agent 9, frontend-app 8, tracebloc-py-package 5, org-config 5, cli 5, tracebloc-website 4, client 4, .github 4, docs 3, client-runtime 3, quickstart 2, design-system 2, data-ingestors 2 — advance to Ready for prod. This batch includes org-config's and quickstart's first-ever promotions through the train.

Note on the journey verdict. Three candidate baselines (tabular/image/text_classification) were dispatched on the staging candidate chart (v1.9.117-rc.1) to prove training end-to-end. The first pass, run on staging's own harness, could not complete: model-zoo was renamed to model-zoo-archive during today's org restructuring, and five e2e-test-agent workflows still hardcode the old checkout path — every leg died at "Fetch the model the journey trains" before reaching admission or Train. Already fixed on develop (e2e-test-agent#625) but not yet promoted to staging. A second pass was dispatched on develop's ref (carrying the fix) while still installing the staging candidate chart, to avoid recutting staging and expanding this batch's scope mid-review. Those runs were still in their Train step, verdict not yet landed, when the reviewer instructed proceeding regardless. This record does not claim a positive training-path verdict for this batch; it is stated here for the record rather than omitted.

What was independently confirmed clean before that decision:

  • Channels (fr-assist, run 34603176353, 13:14 UTC): all OK, one skipped for credentials.
  • Per-repo staging CI green on every tip at review time: backend Tests/docker-build/Lint/Migrations; client CodeQL/E2E mysql/E2E auth-proxy/Release Helm Chart; client-runtime Publish images/Tests; design-system(-v2) Tests/Publish to GitHub Packages; frontend-app Tests + staging smoke (run 34609086840, green); tracebloc-py-package Test Suite + TestPyPI rc; cli CodeQL/Release; tracebloc-website; docs/.github CodeQL/Selftests; data-ingestors CodeQL/Publish internal images; org-config Selftests.
  • Engine's staging tip carried a cancelled post-merge test (3.11) run, superseded by the PR's own pre-merge required-check pass (gate SUCCESS, Bugbot SUCCESS per the train's merge log) plus the same commit's vision-cv-slim-deps succeeding independently — not a live regression.
  • All 93 PR bodies read for holds, interactive/TTY behaviour, or unshipped breaking changes: every flagged phrase (Storybook's "interactive" story, a TypeErrors substring match, stale "not yet merged" text about an already-merged PR, unchecked template checkboxes, mutation-test prose using "revert") was a false positive on inspection. No genuine hold found.
  • e2e-test-agent's Unit tests red on staging (also model-zoo-rename fallout, same chore(deps): bump sigstore/cosign-installer from 3.9.1 to 4.1.2 #625 fix) is unrelated to any of the 93 tickets' own code.

Not independently verified: the training-admission path on the exact staging chart via staging's own harness (superseded by the develop-ref run described above, which had not reached a verdict); client-runtime#553's GPU decline-fallback logic (no GPU in the CPU journey, unchanged from prior batches).

What this evidence cannot tell you (README → Functional review): per-ticket behaviour beyond the journey's path, anything after a failing step, and steps a run skipped.

@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-staging branch September 11, 2026 16:11
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 11, 2026 16:12
@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-staging branch September 11, 2026 16:56
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 11, 2026 16:56
@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-staging branch September 11, 2026 17:07
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-staging branch September 11, 2026 17:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-nudge Toggled by the release train to (re-)fire the fr-gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants