release-train: develop -> staging - #661
Conversation
… 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>
|
bugbot run |
There was a problem hiding this comment.
✅ 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.
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 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: What was independently confirmed clean before that decision:
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); 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. |
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.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 loginno longer always starts the device flow when credentials already exist for the target backend. Unless--forceis passed, it looks up the stored profile (including case-folded env keys like"Dev"), skips locally expired tokens, probes the session withWhoAmI, and on success exits 0 with “Already signed in” while switchingcurrent_envto the reused profile key. Rejected, unreachable, or upgrade-required (426) outcomes are handled distinctly (sharedclassifyWhoAmIErrorwithauth status --check); cancel during the probe exits 130.Adds a one-shot
scripts/backfill-releases.shto copy pre-mirror GitHub releases onto the public deliverable mirror (dry-run by default,BINARY_KEEPfor binaries, publish-guard + SHA256 checks, idempotent writes), plusscripts/tests/backfill-releases-verify.sh(including--mutations) and CI shellcheck / harness steps inbuild.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.