Skip to content

release-train: staging -> main - #526

Merged
tracebloc-release-train[bot] merged 16 commits into
mainfrom
release-train/to-main
Aug 20, 2026
Merged

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

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 18, 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

Medium Risk
Changes affect authentication polling, cluster error messaging, doctor exit codes, and visible client create—security-adjacent and installer-critical paths—but behavior is heavily gated (three-valued residency, confirmed reach only, mint guards unchanged) and covered by extensive new tests.

Overview
Release 0.10.9 promotes a batch of CLI fixes and UX changes from staging, centered on wrong or stale active-client pointers (#515), device-login polling (#517), and guided ingest prompts (#504).

Stale / wrong active client (#515) makes client create user-visible again as the supported way to adopt a client already on the cluster (repoint without minting). client list separates selection from residency via a three-valued cluster_id comparison and only suggests client create when a client is provably on this cluster. Exit-4 / §7.3 paths survey the reached cluster before advising: local single-client clusters get client create; remote/shared clusters get --namespace only. Doctor and the home screen re-probe the kubeconfig namespace on local clusters when the bound pointer misses, use reachConfirmedOK (not != ReachNoEnv) before trusting a re-probe, and treat stale pointers as not ready (exit 2) instead of “everything looks good.”

Login (#517) retries transient PollToken failures (DNS, 5xx, etc.) with a consecutive-failure cap, names the sign-in code TTL in expiry copy, and suppresses tracebloc login follow-up advice when TRACEBLOC_INSTALLER is set.

Guided data ingest (#504) drops bare ? prompts: survey labels are short nouns (Path:, Task:, Label: / Target:) with questions in step headers; tests enforce the label contract.

sanitizeClientName also strips SS3 cursor-mode escapes (#516) and rejects escape-only residue before slugging namespaces. Copy-catalog harvesting folds string-literal concatenations so split fmt.Errorf messages stay inventoried.

Docs (STYLE.md, cli-navigation.md, BUGBOT.md) and version bump align with the above.

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

LukasWodkaand others added 6 commits August 17, 2026 13:37
…nswer (cli#504) (#518)
* feat(interactive): the guided `?` line carries a label, not just an answer (cli#504)
The guided flow built its prompter as `surveyPrompter{bare: true}`, which set
survey's Message to "". The prompt line rendered as a lone `?` — and since #505
started pre-filling answers, as `? [~/mydata]`: a question mark, a bracket and a
path, with no verb.
The bare mode's premise was sound (the CLI already prints `Step 3 of 4 · Where
is your data?`, so repeating it on the `?` line would duplicate it) but the
conclusion was not — and the codebase already said so. `Confirm` refused to go
bare because "a bare `? (y/N)` there would be a label-less destructive prompt";
that objection was never Confirm-specific.
Each guided prompt now passes a short noun label: the shortest noun phrase that
names the answer, with a trailing colon. `? Path: ~/mydata`, `? Task:
tabular_classification`, `? Column types:`. The header still asks the question;
the label says what you are typing into. The label-column question keeps its
two wordings on both lines — `Label:` for a class, `Target:` for a numeric
value — so the branches stay distinguishable on the prompt line too.
`bare` is deleted rather than left unused, so no future call site can reach the
label-less rendering.
Flows with no step header of their own (client create, delete, resources set)
are untouched: they still pass the whole question, which is right for them.
Tests: the ~110 scripted answers keyed by prompt label are rekeyed across
interactive_test.go, copy_catalog_test.go and task_scope_test.go (the issue's
file list missed the third; path_examples_test.go turned out to key on
nothing). Two assertions were rewritten rather than rekeyed, because rekeying
would have made them vacuous: the #181 file-or-folder copy check now reads the
PRINTED step (a short label cannot carry that sentence), and the MLM
no-label-question check names both `Label:` and `Target:` instead of matching a
shared "Which column holds" stem that no longer exists.
New guard TestRunInteractive_EveryGuidedPromptCarriesAShortLabel drives the
real flow across seven scenarios and asserts a property of whatever it asks —
non-empty, ends in ':', carries no '?', within a 16-rune budget — with the
confirm asserted to be the opposite (a whole question). Nothing is scripted by
label, so there is no list agreeing with itself; zero recorded prompts is a
failure, not a pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(release): bump VERSION to 0.10.9 (cli#504)
version-bump-gate failed this PR: v0.10.8 is already released and the diff
touches published paths (internal/*). The release train reads VERSION and cuts
the tag from it — it never bumps for you, so leaving it stale does not fail
here, it fails the next prod hop days later on somebody else (backend#1561).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…520)
* fix(sanitize): strip SS3 escapes and floor escape-only names (cli#516)
sanitizeClientName handled CSI (ESC '[' … final) only. SS3 (ESC 'O' final) is
what the same arrow / Home / End / F-keys emit once the terminal is in DECCKM
application-cursor mode — the state vim, less or tmux leave behind on an unclean
exit. That residue was worse than the CSI residue fixed in cli#364 / client#362
(2026-07-21, not re-litigated here): CSI cleans to empty and re-prompts, while
'O' and the final byte are printable, so ESC OD ×3 ESC OA ×3 survived as the
plausible name "ODODODOAOAOA" and minted the permanent namespace "odododoaoaoa".
Nothing downstream can refuse it: is_dns1123_label validates by idempotence
against the slug rule, so escape-derived garbage is a perfectly canonical label.
Form is exactly what this input preserves.
Two changes, both in sanitizeClientName — deliberately NOT in internal/slug,
which must stay a faithful mirror of backend/common/utils/slug.py:
1. escSequence now matches CSI and SS3 in one pattern.
2. A post-sanitise floor. If an ESC SURVIVES step 1 the value carries an escape
family we do not recognise — which is precisely how SS3 got here — so it
must show one alphanumeric that did not come from an escape final byte,
probed with a greedier pattern whose output is never returned. Nothing but
residue returns "", the same path an omitted --name takes. Scoped to "an ESC
survived" so a clean name never reaches it and real content beside an
unknown escape is kept; the failure it chooses is the recoverable one.
Tests: 10 new cases in the table (SS3 arrows / Home-End / F-keys / mixed with
CSI / truncated / a bare O is not an escape; the floor with SS2 standing in for
"the next family", including the non-Latin-content case) plus a test pinning the
ticket's exact repro and the slug it used to mint.
Mutation-proven, three anchors, each applied and each detected:
• SS3 dropped from escSequence -> 2 cases red ("na\x1bODme", SS3+CSI mixed)
• floor short-circuited to false -> 2 cases red (truncated SS3, unknown family)
• hasAlphanumeric made ASCII-only -> 1 case red (non-Latin content)
The "SS3 arrows only" case is green under anchor 1 because the floor also covers
it; anchor 1 is carried by the mixed-content cases, which the floor cannot mask.
The bash and PowerShell peers get the same two changes in tracebloc/client.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(release): VERSION 0.10.8 -> 0.10.9 (cli#516)
version-bump-gate is a required check and it refuses a PR that touches
internal/* while VERSION still names an already-released version: v0.10.8 is
out, so shipping this fix under it would put different bytes under an existing
release. 0.10.9 is untagged and above every released final version, and it is
the same target the other two open PRs on develop bump to — identical one-line
changes merge without conflict, and all three then ship under the pending
0.10.9.
Not a hand-cut release: the release train still reads this file and cuts the tag
from it at the prod hop. The gate's own message is explicit that it never bumps
for you, and that a stale VERSION fails days later on somebody else's hop
(backend#1561) rather than here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(sanitize): bound the floor's probe to two final bytes (cli#516)
Bugbot, Medium, on tracebloc/client#736: the floor's probe used an unbounded
`[A-Za-z~]+` after an unrecognised ESC, so every ASCII letter following the
escape was swallowed into the probe and the value read as residue-only. It is
right, and the sharper half of it is the part I had not seen: `\x1bNChello` was
refused while `\x1bNC日本` was kept, which makes keep-vs-reject depend on the
script the user's name is written in. I had accepted the over-strictness on
purpose; I had not noticed it was inconsistent.
Bounded to `{1,2}`. Two, not one and not unbounded: one leaves the 'D' of an
unrecognised SS3-shaped pair behind and the floor stops firing on the exact
family shape this ticket is about, while unbounded eats a whole name. An escape
final is one byte, an intro plus a final is two, and every keyboard-input escape
family (SS2, SS3, the 7-bit C1 forms) fits in that — so the bound is a statement
about escapes rather than a tuning constant.
Every case the floor is meant to catch is unaffected: ESC N B / ESC N C, a
truncated ESC O, and ESC [ ; ] A all still collapse to empty.
Applied to all three copies so the rule stays one rule.
Mutation-proven: reverting `{1,2}` to `+` turns the new case red in Go
("\x1bNChello" -> "") and in bats.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…s a way back (cli#515) (#519)
* fix(cluster): say which namespace, instead of offering --namespace blindly
The §7.3 binding-miss error is RFC-0001's sentence with the remedy deleted:
it names --namespace but never says WHICH namespace, and nothing else in the
CLI will tell you. On a laptop with a healthy local install that left no
supported way back.
explain now diagnoses before advising. A binding-applied noParentReleaseError
carries the clientset and server URL of the cluster that actually missed, and
explain spends one naming-only cluster.FindClientNamespaces — the same read
discoverRelease already spends purely to write a better message — then branches
on isLocalServerURL:
one client + local server URL name it, offer `client create` (a re-run on a
cluster that already hosts a client adopts it,
so the repoint mints nothing)
client(s) on a remote cluster name the namespaces, offer ONLY --namespace;
never `client create` there, because the client
we found may be a colleague's (§7.5)
none, scan clean today's text plus "No tracebloc client is
running on this cluster either", which is when
the installer is the right advice
could not look today's text, byte for byte
The last branch is the point of the three-valued clientSurvey: a nil probe or a
failed scan is an absence of evidence, and printing it as "nothing is running
here" would tell a user with a working client the opposite of the truth.
allowScan() is untouched and still false for an applied binding: this changes
what the CLI says, never what it targets. TestActiveClientBinding_AllowScan and
TestDiscoverRelease_NoScanWhenExplicit pass unmodified, and
TestExplain_BindingMiss_NamesTheLocalClientWithoutRetargeting pins both halves
at once — the namespace appears in the message and nowhere else.
The probe travels on the error rather than through explain's signature so a
caller cannot hand it a clientset for a different cluster than the one that
missed; six of the seven call sites never held one anyway (resolveClusterTarget
builds it internally and returns nil on the error path).
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(doctor,home): a wrong pointer is not proof there is no environment
#401 taught the home screen that an EMPTY active-client pointer says nothing
about what runs on this machine. The wrong-pointer case was never covered, and
it is the worse one: doctor binds the stale pointer, probes only that namespace,
never scans, and prints "No secure environment on this machine yet" with the
installer command underneath — over a perfectly healthy install. home has the
same hole from the other side: its local-env fallback sat behind
`if !binding.applied`, so a non-empty pointer skipped the #401 fix entirely.
Both now route the miss through the same fallback:
home the ErrNoParentRelease branch returns localEnvFallback(ctx) instead of
a bare localNoRelease. Every failure inside the fallback degrades to
localNoRelease, so this branch's old return value is still its floor.
doctor on a ReachNoEnv result that a BINDING (not the user) aimed, re-probe
the namespace the kubeconfig itself selects, via localEnvNamespace.
The ownership gate is what makes this safe, and it is unchanged: both adopt only
when isLocalServerURL says the kubeconfig's server is this machine — a cluster
that is this machine by definition, so whatever runs there is this machine's
environment. On a remote or shared cluster the honest no-environment answer
stands, and a colleague's client is never greeted as yours (§7.5).
No scan is spent either: the installer points the kubeconfig context at the
client's namespace (client/scripts/lib/install-client-helm.sh runs `kubectl
config set-context --current --namespace <ns>`), so reading the context is
enough to find a healthy install that the pointer missed.
doctor keeps the original results unless the re-probe actually finds an
environment, so a genuinely bare machine still gets the installer advice and the
--diagnose bundle still describes the namespace the user is configured for. An
explicit --namespace is never second-guessed — no binding, no re-probe.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(client): list `create`, and stop calling a stale pointer "this machine"
Two things the diagnose-before-advising fix needs in order to reach anyone.
`client create` is visible. It was Hidden because a human running it standalone
on a cluster with no client mints one the installer never deploys — an orphaned
phantom (backend#970). That risk is real and unchanged, but hiding the command
was never what prevented it. The two guards that do are untouched, and now have
tests naming them:
• on a TTY, the review + "Provision this client?" confirm. A re-run on a
cluster that already hosts a client never reaches it — adoption happens
first — so the repoint stays prompt-free and mints nothing.
• off a TTY, a hard refusal without --yes/--credential-file, so a pipe or CI
can never mint silently.
What hiding did cost is #515: the one command that repoints a machine was
unlisted, so the error telling a user to repoint pointed at nothing they could
find. Its Short/Long now describe what it does for a user (adopt/repoint) rather
than the installer's use of it. `client list` stays hidden.
`client list` marks residency, not just selection. It labelled the active
pointer "(active — this machine)" without ever checking where that client runs —
so in exactly the state this ticket is about, the listing sat there confirming a
client provably not on this machine. Selection (the local pointer) and residency
(does it run on the cluster the kubeconfig reaches, keyed on the §7.2 cluster
anchor) are now two separate facts, and a mismatch names the repoint.
An unreadable anchor is a third state, not a "no": with no kubeconfig or an
unreachable API server, no row claims to be here and none is denied — the marker
degrades to bare "(active)". The installer's #303 pre-flight is unaffected; the
markers sit in the row label and the greppable `namespace=<ns>` field is
untouched (client_list_contract_test.go still passes).
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(cli): regenerate the copy catalog; `client create` is no longer hidden
The golden diff is the whole user-visible change, reviewed line by line:
08-client.golden `create` now appears under Available Commands, with its
new Short and the Long that explains adoption.
zz-all-strings.golden the five §7.3 branches and the `client list` mismatch
hint.
Each branch of repointMessage is one format literal rather than a `+`-joined
string, because the catalog's AST harvest only sees literal arguments — the
message it replaced was invisible there for exactly that reason, and half a
sentence in the completeness backstop is worse than none.
cli-navigation.md carried two statements this change makes false: it drew
`client create` as a hidden node, and its exit-4 remedy line said "run the
installer (or --namespace)", which is now only one of three answers.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(release): bump VERSION to 0.10.9
v0.10.8 is already released and this PR changes published files under
`internal/*`, so version-bump-gate (a required check) asks for the bump here
rather than leaving it to fail the next prod hop on somebody else
(backend#1561). The release train reads VERSION and cuts the tag from it.
Patch, matching this repo's dominant pattern for user-facing copy and surface
changes — say so on the PR if 0.11.0 is wanted for the `client create` unhide.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(cluster): say why explain replaces the error instead of wrapping it
`%w` is the house convention (~325 sites), which makes a bare errors.New here
read as an oversight. It is the same deliberate replacement the fmt.Errorf it
replaced did: the §7.3 guidance is meant to BE the message, not to trail the raw
"no release in namespace X". Wrapping would also make the result re-match
errors.As(*noParentReleaseError) and so re-explainable. Recorded in place so a
reviewer doesn't have to re-derive it.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(doctor,home,client): finding the environment is only half the story
Both Bugbot findings on this PR were right, and both are the same mistake:
a two-valued answer where the honest answer has three values.
HIGH — "stale pointer still blocks after fallback". Re-probing found the
healthy environment and then said nothing about the pointer that missed. So
doctor printed "Everything looks good — you're ready to run training" and exited
0 while `data`/`resources`/`seal` all still bind the stale namespace and exit 4;
before this PR that state at least exited 3. doctor now names the stale pointer
and the repoint, and exits 2 — the code it already uses for every actionable
finding. A problem WAS found; it just isn't in the cluster.
The home half was worse, and was newly introduced here: local liveness came from
the fallback's client while the heartbeat is still looked up by the STALE
client's id, so a colleague's machine being online could render this one green.
envProbe carries pointerStale, and resolveHomeModel refuses both directions off
it — a stale heartbeat can no longer green the screen, nor harden into
"backend reports not online" for a client it isn't about. It drops to "running,
couldn't confirm", which is exactly true.
MEDIUM — "empty cluster ID marked absent". `client list` compared anchors as a
boolean, so a client whose OWN anchor is empty — legacy / not-yet-backfilled,
which api.ProvisionedClient documents — was reported as "NOT on the cluster your
kubeconfig reaches", with the repoint hint, possibly while running on this very
machine. Exactly the collapse this PR's cluster-anchor handling was careful to
avoid, missed one level down. Residency is now a three-valued residencyOf():
either anchor missing is resUnknown, and unknown claims nothing either way.
realProbeEnv moved to home_local_fallback.go: home.go went 13 lines over its
file budget, and the probe is now mostly a decision about WHICH fallback to
take, so it reads better beside them than beside the renderer.
Six mutations, each with its anchor asserted and each reddening an assertion
rather than the compiler: collapse the empty client anchor; let doctor green a
stale pointer; drop doctor's stale-pointer note; stop marking the fallback's
result stale; let a stale pointer render Online; let another client's not-online
harden into a verdict. TestDoctor_HealthyPointer_StillGreen is the control —
without it, "never says Everything looks good" would pass vacuously.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(doctor): put the stale-pointer finding IN the readiness line
Rendering the previous commit showed the fix was half done: the closing verdict
was honest, but the line above it still read
✔ Connected to tracebloc
✔ Ready to run training
⚠ Your active client points at namespace "stale-ns" …
— a green tick and, directly beneath it, a warning saying the opposite. That is
the same unearned success the finding was about, moved up the screen.
`Ready to run training` is false whenever the pointer is stale, however green
the cluster checks are, because every data command binds the pointer. So the
readiness healthLine is replaced rather than accompanied, and it carries the
remedy, so the finding and the fix read as one thing:
✔ Connected to tracebloc
✖ Not ready — your active client points at namespace "stale-ns", which
isn't on this cluster, so data commands will keep failing until you
repoint.
Point this machine at the environment above: tracebloc client create
(this cluster already runs a client, so it adopts it — no new credential)
Phrased "Not ready — …" to match the three readiness failures already in the
catalog. `--diagnose` records the replaced line, which is what triage needs.
Exit stays 2 via the pointerStale branch, which skips the "email support" nudge
a doctorVerdict fail would add — we just gave a precise one-command fix.
Three more mutations: disable the replacement (green tick returns) → red;
drop the remedy → red; stop naming the stale namespace → red. The
already-added TestDoctor_HealthyPointer_StillGreen now also asserts the green
tick IS present when nothing is stale, so "no green tick" can't pass by the
line disappearing entirely.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(client,home): don't name a target we haven't confirmed exists
Bugbot round 2, two Mediums, and the same class a third time: a sentence
asserting something the code never established.
`client list` set the mismatch hint from the ACTIVE row alone, so with the
pointer elsewhere and nothing confirmed on this cluster it still said "point
this machine at the client that IS there: … client create". There may be no
client there — and on a cluster with none, `client create` falls through to the
MINT path and produces the orphaned phantom backend#970 exists to prevent. So
this PR's own advice could manufacture the bug the command was hidden for.
The repoint is now offered only when some row is resHere — that is what earns
the phrase "the client that IS there". Otherwise the mismatch is still reported,
without a target: "no client here is confirmed. Check your kubeconfig context,
then run: … doctor". Deliberately covers BOTH remaining cases, because they are
equally unnameable — no client here at all, and clients that might be here but
carry no anchor to prove it (resUnknown).
The home screen had the label version of the same thing: with a stale pointer,
`env.name` was still overridden by the remembered handle, so the client the
pointer names was printed as the environment running here — a client that is by
construction NOT what the fallback found, and a name contradicting doctor's for
the identical state. The override is now skipped when the pointer is stale, so
the screen falls through to the probe's own name for the release that is
actually running.
Three mutations: let the repoint hint fire without anyHere → red; source anyHere
from the active row instead of residency → red; restore the unconditional
remembered-name override → red. Both fixes carry a control assertion in the same
test (the repoint IS offered when a row is here; the remembered name IS still
preferred when the pointer is fresh), so neither can pass by the behaviour
disappearing altogether. N10's first attempt left `anyHere` unused and reddened
the compiler; rewritten to keep it used and re-run.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(doctor): adopt the re-probe only on a CONFIRMED reachable cluster
Bugbot round 3, High, and correct. The re-probe adopted on
`reachStateOf(retry) != doctor.ReachNoEnv`, and ReachState has four members:
ReachUnreachable and ReachError also satisfy that. Both mean "we could not
tell". So a stale pointer plus RBAC on the context namespace, or a transient
read failure, would have:
• named an unconfirmed namespace as `Secure environment "…"`,
• set pointerStale and printed "this cluster already runs a client, so it
adopts it — no new credential",
• and sent the user to `client create` on a cluster that may host nothing,
where it does not adopt but MINTS — the backend#970 phantom.
Which is the exact absence-as-presence collapse surveyCluster's `looked` and
residencyOf's resUnknown exist to prevent, made twice more in the same PR.
Adoption now requires a positive confirmation, via reachConfirmedOK(). It is
deliberately NOT `reachStateOf(results) == ReachOK`: reachStateOf defaults to
ReachOK when the check is ABSENT, which is the right lenient default on the main
path and precisely the wrong one here, where the whole question is whether an
unproven namespace may be believed. Absent, unreachable and errored all answer
"could not tell", and none may authorize naming an environment or advising a
repoint.
The test derives its input domain from doctor.ReachState's declared surface —
every non-OK member, plus the absent case — rather than picking the states that
came to mind: mutation coverage cannot see a vocabulary gap, so a future member
has to be added to the enum's own list to escape it.
Three mutations: restore `!= ReachNoEnv` → three subtests red; make an absent
check count as confirmed → red; let ReachError confirm → red.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(bugbot): make the recurring finding on this PR a rule
Org standard: "A finding that recurs across PRs becomes a rule: add it to
.cursor/BUGBOT.md". This one recurred three times inside a SINGLE PR — a failed
scan read as "nothing is here", an empty legacy cluster_id read as "runs
elsewhere", and `!= ReachNoEnv` read as "an environment is here" — each found by
Bugbot only after the previous was fixed. Three instances of one root cause is
past the threshold.
BUGBOT.md already had the neighbouring rule, but scoped to the value a function
RETURNS ("prefer a three-valued return"). Every instance here got the return
type right and then collapsed it at the `if` that consumed it, so the existing
bullet didn't catch any of them. The new bullet is about the branch, and names
the two concrete shapes rather than restating the principle:
• a negated comparison against ONE member of a multi-valued enum, which
silently absorbs every member added later — with the corollary that the
test's input domain must come from the enum's declared surface, since
mutation coverage cannot see a vocabulary gap;
• a lenient "not found" default reused where the question is "may I believe
this?" — reachStateOf returning ReachOK for an ABSENT check is right for a
verdict roll-up and wrong for authorising a claim, which is why
reachConfirmedOK exists beside it.
It closes with the customer-visible cost, per this file's own Tone section: each
instance ended in advice to run `client create` on a cluster nothing was
confirmed on, where it mints instead of adopting — the guidance manufacturing
the orphaned phantom backend#970 is about.
Refs cli#515
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…e window (#517) (#521)
* fix(auth): a transient poll failure retries; the expiry copy names the window
cli#517. The device-poll loop's `default:` branch was terminal, so anything
that was not one of the four RFC 8628 sentinels ended the sign-in — a DNS
blip, a backend restart, a proxy 502. Inside a ten-minute human-paced window
that is a long exposure, and under the installer it threw away a run that had
already built a cluster.
The default is inverted: unknown failures retry, and every terminal state is
now enumerated in classifyPollError — the four sentinels, a 426 version floor,
a cancelled context, and any *APIError that is not 5xx / 408 / 429. So a
server's refusal still stops on the first poll; only failures that never
reached a verdict are ridden out. Retries are bounded by maxPollFailures
consecutive failures (reset by any answer), so an unreachable backend reports
itself instead of burning the code's window and then blaming the user.
Also from #517:
• the expiry message names the window ("sign-in codes are valid for 10
minutes"), derived from the server's expires_in rather than hardcoded —
without it a ten-minute timeout reads as an instant failure;
• "Run `tracebloc login` to start a new one" is suppressed when
TRACEBLOC_INSTALLER is set. That advice is right for a hand-typed login
and wrong under the installer, which prints its own next step; the two
used to contradict each other on screen.
• a Ctrl-C landing mid-request now exits quietly, like one landing between
polls, instead of reporting the operator's interrupt as a sign-in failure.
Every message stays a literal argument of errors.New / fmt.Errorf so the copy
catalog's AST harvest can still see it; TestCopyCatalogSeesTheSignInStrings
pins that, because composing copy inside a helper drops it from the catalog
silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(release): VERSION 0.10.8 -> 0.10.9
version-bump-gate: v0.10.8 is already released and this PR changes a published
path (internal/*), so the train would otherwise cut the next tag from a stale
file. 0.10.9 is the same pending version cli#518, #519 and #520 bump to — they
all ship under it together, and the identical change merges without conflict.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…104 invisible) (#522)
* fix(test): the copy catalog skipped every message written as a join
harvestMessages type-asserted arguments straight to *ast.BasicLit, so a message
split across source lines —
fmt.Errorf("unknown backend environment %q — valid values are … "+
"set CLIENT_ENV or pass --env", env)
— is an *ast.BinaryExpr and was skipped ENTIRELY. Not the second half: the whole
message. This file's own header calls the golden "the completeness backstop",
and it passed forever while a whole syntactic class of copy was invisible to it.
104 previously-unseen messages, 0 removed. They are not marginal — they are the
long validation errors that tell a user how to fix their data: the BOM in an
Excel "CSV UTF-8" export, non-UTF-8 CSVs, masks that don't match the image
resolution, labels.csv rows referencing absent images, symlinks in the dataset
tree. The copy most worth guarding against drift was the copy the guard could
not see.
literalString folds ADD chains of literals (and parenthesised ones), refusing any
join with a non-literal operand. That refusal is the load-bearing half: emitting
the literal fragments of a part-computed message would put a sentence in the
catalog that no user ever sees, and mark it inventoried while the real text
drifts. Absent is honest; half is not.
Proven in BOTH directions on the same mutation — breaking the reported message in
auth.go:
with the fix TestCopyCatalog FAILS
without the fix TestCopyCatalog passes <- the guard could not see it
TestLiteralString pins the fold with inputs written down independently of the
matcher, so a typo in one cannot plant itself in the other; reverting the fold
reddens 5 of its cases. TestHarvestMessages_SeesConcatenatedCopy pins the
reported defect itself.
Found while doing cli#517 (#521), where new copy composed inside a helper
vanished from the catalog the same way; that PR worked around it by keeping
every sentence a direct literal argument. This is the underlying scanner gap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(version): 0.10.8 -> 0.10.9
version-bump-gate fails any PR touching a published path while the current
VERSION names a released tag, and its publish glob is `internal/*` — which
matches internal/cli/copy_catalog_test.go even though a _test.go file ships
nothing. 0.10.9 is being cut regardless (cli#518, #519, #520 and #521 all bump
to it), so this change genuinely rides under that version; the identical one-line
edit merges without conflict.
Preferred over the skip-version-gate override: the label is for a false positive
nobody should have to reason about later, and the honest statement here is that
this is part of 0.10.9.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot 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 f9f1328. Configure here.

@tracebloc-release-traintracebloc-release-trainBot 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 18, 2026
LukasWodkaand others added 4 commits August 18, 2026 10:32
…cosign (#523)
install.ps1 gated cosign verification on `if (Get-Command cosign)`. On a
fresh Windows box cosign is never present, so the default path printed
(cosign not installed; SHA256 verified, signature skipped)
and installed. The SHA256 it kept is same-channel — SHA256SUMS ships from
the same release as the binary, so whoever could swap one could swap the
other. It proves the download finished, not who built it.
That made README.md:75 ("Signature verification is mandatory ... fails
closed", no platform qualifier) false on Windows, and only on Windows:
install.sh has bootstrapped a pinned cosign and failed closed since
backend#889. The README needs no edit — this makes the existing claim
true.
Windows now does what Linux and macOS do: bootstrap a pinned,
checksum-verified cosign, and refuse to install when it can't.
TRACEBLOC_ALLOW_UNVERIFIED=1 is the one escape, and it covers "cannot
verify" only — never a verification that ran and FAILED, which is
tampering evidence no env var should override.
Two things this does that install.sh does not have to:
- The bootstrap fetches cosign-windows-amd64.exe on BOTH architectures.
Sigstore has never published a Windows arm64 build, so a per-arch name
404s and locks Windows-on-ARM out permanently (the same bug as
tracebloc/client#734). Verification is over bytes, so the verifier's
instruction set cannot change the verdict.
- Test-CosignRuns separates "cosign won't start here" from "the signature
is bad". They arrive through the same channel and warrant opposite
messages — only one means the artifact may be tampered with. Absent x64
emulation on Windows-on-ARM is the case that makes this real, and
"install cosign" would be useless advice there.
Also sets a TLS 1.2 floor: PS 5.1 defaults to SSL3/TLS1.0 on older
Windows, and every fetch here carries either the binary or the verifier
that authenticates it.
Tests — scripts/tests/install-ps1-verify.sh, install-verify.sh's sibling,
wired into the same CI job. Against develop's installer: 0 passed, 5
failed. After: 5 passed, 0 failed.
install.ps1 cannot be driven end-to-end on the Linux runner (it ends in
Windows-registry PATH writes), so the behavioural tier extracts the
helpers from the real file BY AST and executes them — a copy would pass
while production broke. Six guards, each mutation-proven, each mutation
asserted to have applied:
[bool]$env:... instead of -eq '1' -> 4 fail
checksum mismatch stops refusing -> 1 fail
$LASTEXITCODE not armed before the probe -> 1 fail
an opt-out on a FAILED verification -> 1 fail
the no-cosign degrade restored -> 1 fail
a \$ escape in a message -> 1 fail
Three of those are bugs this change made and this tier caught before
review:
- $AllowUnverified was [bool]$env:TRACEBLOC_ALLOW_UNVERIFIED. Every
non-empty string casts to $true in PowerShell, so setting it to 0 would
have switched the bypass ON.
- The bootstrap restated Get-Arch's logic minus its PROCESSOR_ARCHITEW6432
handling, so a 32-bit PowerShell host would have refused. Removed: the
asset is arch-independent, there was nothing to branch on.
- "Pin a signed \$env:RELEASE_VERSION" rendered as "Pin a signed ," —
PowerShell escapes with a backtick, so \$ prints a backslash and expands
the variable. Now a check of its own.
One test in this tier was vacuous on its first writing: a "stale exit
code" assertion built on an absent path, which throws and returns before
ever reading $LASTEXITCODE. The mutation caught it — M3 applied and
nothing reddened. Replaced with the input that is actually reachable: a
PowerShell shim (scoop and chocolatey install cosign.ps1), which `&`
dispatches in-process and which sets no $LASTEXITCODE at all.
backend#2078
… path (backend#1907) (#527)
The CLI emits nothing today, so the backend#736 class — the binary landing on a
PATH the shell does not read, `cluster info` reading a kubeconfig context nobody
meant — is only ever visible when a customer mentions it. This wires the #1897
helper to a single terminal event per invocation: command, duration, exit code,
OS/arch, version, error class.
The ticket's "no arguments, no paths, no data" is built as a structure rather
than a rule, because a rule is a thing every future call site has to remember:
* the command is a LOOKUP into the set of paths enumerated from the live cobra
tree, so a value that is not a command the CLI dispatches cannot be emitted
at all — it reports `unregistered`, which stays countable;
* the error class is keyed on an INT, the CLI's own frozen exit-code contract.
The classifier is never handed an error message, so there is nothing for a
path or a cell value to travel down;
* everything else is an int.
There is no redaction regex anywhere in the change. A sanitiser has to
anticipate what it strips; a closed set only admits what was enumerated.
os.type / host.arch go in the RESOURCE layer under OpenTelemetry's own names
(§1.1 forbids re-inventing them as tracebloc.os): they are compile-time
constants of the binary, so they describe the process, not the occurrence, and
adding them to resourceScope means a call site still cannot set them.
The guard is derived, not restated. TestEveryEmittedStringComesFromAClosedSet
walks what the code ACTUALLY emits and requires every value to be an int or a
member of a set assembled from the producer's own declarations — so a free-text
channel fails it whether or not anyone thought to forbid the thing travelling
down it. Thirteen mutations were run against it; each reddened, and each anchor
was asserted to have applied. One of them (smuggling the raw command into a
second attribute) was caught only by the telemetry-side test and NOT by the
cli-side one, which was inspecting a single key — that test now sweeps the whole
payload.
WHAT IS NOT CONNECTED. The transport. The ticket says "rides the gateway and
token"; the 17 Aug decision (rfcs#28) replaced the Collector gateway with an
ingest endpoint on the backend, which is backend#1905 and does not exist yet.
pendingSink() returns nil, so every event is validated and dropped. Validation
runs regardless, so a malformed event fails in CI wherever the binary was built,
and connecting #1905 is one function body.
Opt-out (default on) via TRACEBLOC_NO_TELEMETRY or DO_NOT_TRACK, documented in
docs/troubleshooting.md — and the document's claim about which variables work is
itself a test, because a user who exports a stale name believes they have opted
out and nothing else would ever tell them.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(install.ps1): verify-blob must not inherit a stale exit code
`$LASTEXITCODE` persists from the previous command. `Test-CosignRuns` runs
`cosign version` immediately before the verification, and presets 255 precisely
so a binary that never starts cannot leave a stale 0 behind.
The `verify-blob` call that actually GATES the install had no such preset. So a
cosign shim that exits 0 on `version` and then no-ops on `verify-blob` leaves
`$LASTEXITCODE` at 0, the `-ne 0` gate reads that as success, and the installer
prints "cosign signature valid" and installs a binary nothing verified. That is
RFC-0001 R8 defeated by a stale variable, one line from the guard that exists.
Reproduced before fixing, driving the real block extracted from install.ps1 under
pwsh with a no-op verifier:
with fix -> REFUSED (LASTEXITCODE=255)
without fix -> INSTALLED-UNVERIFIED (LASTEXITCODE=0)
The regression assertion is a SOURCE check, and the limitation is stated rather
than hidden: install.ps1 has NO behavioural coverage -- there is no pwsh or
Pester anywhere in this repo's CI, which is why a signature gate that does not
gate reached a promotion PR. Case 19 asserts the preset sits inside the
`$sigDownloaded` block and before the invocation, by line number, so a preset
elsewhere cannot satisfy it. It closes this hole; it does not make the Windows
installer tested. Worth its own ticket.
Mutation-proved: removing the preset reddens case 19 with the message naming the
consequence. 34 passed / 0 failed; the mutation gives 33/1.
One self-inflicted detail recorded because it is the repo's own failure class:
the first version of the check grepped for `verify-blob` and matched the
explanatory comment written directly above the call, so it failed on correct
code. Anchored on the `& $cosign` invocation instead -- prose is not wiring.
Found by Bugbot on release-train promotion PR cli#528 (High).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(install-verify): match the STATEMENT, not the text (shujaatTracebloc, #529)
Both review findings were right, and both are the failure this PR is about --
left open on the side the PR did not anchor.
A COMMENTED-OUT PRESET SATISFIED CASE 19. The check matched the substring
`$global:LASTEXITCODE = 255` anywhere on a line, so `# $global:LASTEXITCODE = 255`
passed it: the gate dead, the suite green. That is the likelier human mutation --
commenting the line out while debugging the installer -- and it was exactly the one
not covered. The PR proved the DELETE mutation and missed this one.
THE MIRROR IMAGE, ON THE SAME LINE. Hard-coded single spaces meant
`$global:LASTEXITCODE=255` -- correct, equivalent PowerShell -- turned case 19 RED
on a working gate, with a message asserting the installer would install unverified.
A false alarm that names a supply-chain failure is worse than none.
Anchoring the whole statement start-to-end, with flexible spacing, closes both.
Suggestion taken as written from the review.
AND THE `$` IS ESCAPED in the `if ($sigDownloaded)` grep, matching the two sibling
patterns in the same block. In a POSIX BRE a `$` that is not at the end is
undefined; an implementation treating it as an anchor matches nothing, `blk_line`
comes back empty, and case 19 fails on correct code.
Verified all three directions on this branch, reproducing the reviewer's results
first:
commented-out preset before 34/0 (escaped) -> after 33/1 (caught)
no-space variant before 33/1 (false) -> after 34/0 (correct)
preset deleted before 33/1 -> after 33/1 (still caught)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The last board writer on PROJECTS_KANBAN_TOKEN. A per-repo COPY, so one PR per
repo; the content stays byte-identical across the fleet because the guard
compares it that way.
`owner:` makes the installation token ORG-scoped -- a repo-scoped one cannot write
the org project. No fallback to the PAT: a fallback would let a broken App path
keep working silently.
This workflow also fires on DEPENDABOT PRs, which GitHub gates on a separate
secret scope. Both app secrets are set there too; without that, Dependabot PRs
would stop reaching the board with `Input required and not supplied` -- the exact
failure PROJECTS_KANBAN_TOKEN already had to be dual-scoped to avoid.
Refs backend#2036
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot 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 f9f1328. Configure here.

@tracebloc-release-traintracebloc-release-trainBot 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 19, 2026
LukasWodkaand others added 3 commits August 19, 2026 11:14
#531)
* fix(telemetry): do not file a run signed into an unknown env under prod
telemetryEnv repaired a present-but-unrecognised signed-in environment through
api.ResolveEnv(""), which returns prod when $CLIENT_ENV is unset. New() then saw
a known env and exported — filing a run signed into an unknown backend under
prod, the exact guess §3.2 forbids and this function's own doc disclaims.
Distinguish the two cases: empty (not signed in) still resolves via $CLIENT_ENV
then the prod default; a present-but-unknown value is passed through unchanged so
New() disables export. Correct TestTheEnvironmentIsNeverGuessed, which asserted
the buggy prod answer for a signed-in "staging", and add an end-to-end regression
(TestASignedInUnknownEnvironmentDeliversNothing).
Bugbot (Medium), cli#528 staging mirror.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(telemetry): label the record with the backend the client actually uses
Reversing the direction of the first commit, per @saadqbal's review. The premise
there — an unknown signed-in env is an unknown backend, so withhold — does not
hold: sessionEnv (client.go) hands cfg.CurrentEnv to api.New verbatim and
api.BaseURL routes every unrecognised value to prod. So a run signed into an
unknown env genuinely hits prod, prod is the ACCURATE label, and withholding
drops exactly the failed-install-on-prod runs this feature exists to see.
telemetryEnv now mirrors api.BaseURL: resolve (CurrentEnv, else $CLIENT_ENV/prod),
then known -> itself, unknown -> prod. The real bug it fixes is the old code
reading $CLIENT_ENV for a signed-in env while the client ignores it — filing a
run under 'dev' while every request went to prod. Rename the param env -> drop the
shadow of signedInEnv(). Tests flip from 'delivers nothing' to 'labelled prod',
plus TestASignedInUnknownEnvIgnoresClientEnv pinning the divergence (fails against
the old code). Root cause — BaseURL silently routing unknown envs to prod — filed
separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#532)
* fix(install.ps1): SET the TLS 1.2 floor, don't OR it onto the default
The floor bitwise-OR-ed Tls12 onto [Net.ServicePointManager]::SecurityProtocol,
which on PowerShell 5.1 already advertises SSL3/TLS1.0/1.1 — so those stay on and
a fetch of the binary or the cosign verifier can still negotiate down, the exact
downgrade the floor's own comment says it prevents (cli#528 Bugbot, Medium).
Assign the protocol to Tls12 (dropping the weak ones), adding Tls13 only where the
runtime defines the enum member (absent on older 5.1 hosts, where naming it
throws). New install-ps1-verify assertion fails on the OR-onto-default form
(mutation-proved); collapses newlines first since the old form spanned two lines.
install-ps1-verify 6/6, behavioural tier 22/22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(install.ps1): set only the Tls12 floor, drop the throwing Tls13 add
Bugbot (Medium) on the first push: [Enum]::IsDefined([Net.SecurityProtocolType],
'Tls13') is true on .NET 4.8 even where Schannel cannot negotiate TLS 1.3 (Win10
21H1, Server 2019). Assigning Tls12 -bor Tls13 then THROWS, the empty catch
swallows it, and SecurityProtocol is never set — so the Tls13 decoration could
defeat the very Tls12 floor it was meant to extend. Assign Tls12 alone: it is the
floor, always negotiable, and secure for these fetches. Verify assertion now pins
the direct Tls12 assignment; still fails on the OR-onto-default form
(mutation-proved). 6/6 verify, 22/22 behavioural.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
LukasWodkaand others added 2 commits August 20, 2026 03:47
Brings this repo's copy to the fleet canonical form. `add-to-kanban.yml` is a
byte-compared copy, so this is one pass over every repo rather than a fix here.
WHAT CHANGES
repositories: ${{ github.event.repository.name }} the two content reads stop
being org-wide
permission-issues: read add-to-project must RESOLVE
permission-pull-requests: read the triggering node before
permission-organization-projects: write it can add it
permissions: {} the job needs no GITHUB_TOKEN
Without any `permission-*` the mint carried the App's FULL installation grant --
contents+PR write across every installed repo -- and the App holds bypass_reviews
on staging and prod fleet-wide, so the blast radius was merge-past-review rather
than merely write.
WHY THE FILE IS BYTE-IDENTICAL TO backend's. That copy is the one that survived
review: saadqbal caught that `owner:` narrows nothing ("Input 'repositories' is not
set. Creating token for all repositories owned by tracebloc"), and aptracebloc
caught a run cited as evidence that was not one. Copying the corrected version
rather than re-deriving it is the point of a byte-compared file.
VERIFIED, not assumed: run 32255581084 on backend#2181's head exercised these exact
scopes and landed the card (Status=Code review), which settles the one open
question -- `repositories:` scoping does not clip `organization_projects`.
Refs backend#2157.
* ci(2212): the fixtures drift check must fail when it cannot run
`Backend fixtures drift check` is being armed as a required context
(backend#2212). Its activation-phase fail-open has to go first: when
BACKEND_CONTRACTS_TOKEN was unreadable the step printed a warning and exited 0,
so a check that never executed reported as a passing one. Inert-not-red was the
right call while the secret did not exist; the secret has existed since
2026-08-05, and once the context is required an exit-0-when-unable is strictly
worse than an advisory guard, because it also looks solved (backend#2183).
`cli` is PUBLIC, so the two reasons the token can be missing are different
things and the step now splits three ways:
token present -> run the check
absent, fork PR -> FAIL. GitHub withholds repo secrets from forks by
design, so the check genuinely cannot run. A maintainer
verifies internal/api/testdata/*.json by hand and
applies `skip-fixtures-drift` -- a permanent artifact on
the PR, the same model as skip-fr-gate. Silently passing
forks would fail open on exactly the contributions that
deserve the most scrutiny.
absent, same-repo -> FAIL. Rotated, removed or expired: a misconfiguration
that used to read as a clean run.
`types: [.., labeled, unlabeled]` added to the pull_request trigger, because
without them the default opened/synchronize/reopened means applying the override
label changes nothing until the next push -- the same defect Bugbot caught on
version-bump-gate-caller.yml's skip-version-gate.
Every ${{ }} goes through env:, none into the run: body.
Mutation-proved, all five paths, by running the step body against a stubbed
sync script:
override label present exit 0 (OVERRIDDEN warning)
token present exit 0 (real check ran)
token absent, fork PR exit 1 (could not run)
token absent, same-repo exit 1 (secret missing)
token present, script reports drift exit 3 (exec propagates the status)
The last one matters: `exec` replaces the shell, so a real drift failure still
fails the step rather than being swallowed.
Label `skip-fixtures-drift` created on this repo.
Refs tracebloc/backend#2212
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(2212): Dependabot PRs are not forks, and would have been blocked
I claimed in chat that this PR was unaffected by the Dependabot finding on
averaging-service#367. Wrong, and this repo is the worse case of the two.
Dependabot branches live in THIS repo, not a fork, so
`github.event.pull_request.head.repo.fork` is FALSE on them -- verified on the
real #530: head.repo.fork=false, head.repo.full_name=tracebloc/cli. Their runs
still receive only Dependabot-scoped secrets, so BACKEND_CONTRACTS_TOKEN is
empty. Under the previous commit that combination landed in the "absent,
same-repo -> misconfiguration -> FAIL" branch, which would have blocked every
Dependabot PR once the context is armed.
Not theoretical: this repo has 4 Dependabot PRs, #530 is OPEN right now, and it
currently reports `Backend fixtures drift check: success` -- the fail-open
passing vacuously on a live PR today.
So Dependabot gets a fourth branch, passing with a ::notice::. Safe for the same
structural reason as averaging-service#367, via a different always-running
guard: a dependency bump cannot alter internal/api/testdata/*.json, and if it
did, internal/api/contracts_test.go replays every fixture through the real
decode paths under the REQUIRED `Test` check with no token. Drift against the
pinned backend ref is re-checked by the push run on develop/main, where Actions
secrets are available.
Mutation-proved, all five:
Dependabot PR (fork=false, no token) exit 0 (notice: deferred)
fork PR, no token exit 1 (could not run)
human same-repo, no token exit 1 (secret missing)
token present exit 0 (real check ran)
override label exit 0 (OVERRIDDEN warning)
PR_AUTHOR uses github.event.pull_request.user.login, not github.actor, so it
stays correct across re-runs.
Refs tracebloc/backend#2212
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot 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 6f66e87. Configure here.

@tracebloc-release-traintracebloc-release-trainBot 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 20, 2026
@tracebloc-release-train
tracebloc-release-trainBot merged commit f92f95c into mainAug 20, 2026
44 checks passed
@tracebloc-release-train
tracebloc-release-trainBot deleted the release-train/to-main branch August 20, 2026 11:00
LukasWodka added a commit that referenced this pull request Aug 20, 2026
`Installer (shell)` is a REQUIRED status check on develop, and its first action was
an `apt-get` with no retry and no time bound of its own. A slow package mirror
therefore consumed the whole 10-minute job budget before any shell was parsed, and
blocked every PR in the repo while doing it.
MEASURED, not theorised. cli#533 is a workflow-only diff that cannot touch installer
behaviour, and it failed FOUR consecutive times:
job 96126585157 Installer (shell) failure 10m16s
15:34 Set up job
15:34 Run actions/checkout
15:34 shellcheck + dash parse <- 10 minutes here, then killed
15:44 Post Run actions/checkout
Nothing after the `apt-get` line ever ran. And the annotation read `The job has
exceeded the maximum execution time of 10m0s` on a job called `Installer (shell)`,
so whoever sees it reasonably concludes the installer is hanging. Nothing points at
apt.
NOT REPO-WIDE, which is worth stating because the ticket first implied it: #530 and
#526 pass the same check. It reproduced on one head, four times.
THE FIX REMOVES THE DEPENDENCY RATHER THAN HARDENING IT. Both tools are already on
`ubuntu-latest`:
* shellcheck is preinstalled -- tracebloc/.github's own `quality / shellcheck`
job, a REQUIRED check in 16 repos, calls `shellcheck --version` with no install
at all. The org has depended on that fleet-wide for as long as that job existed.
* dash IS Ubuntu's /bin/sh, an essential package.
A retry-with-timeout around apt would have been the smaller diff and the worse fix:
a step that installs nothing cannot stall on a mirror, and no wrapper can say that.
`shellcheck --version | head -2` is kept as the first line, matching what the org's
own shellcheck job does -- so the version in use is in the log, and an absent binary
fails on line one with an obvious message instead of somewhere further down.
THIS PR'S OWN RUN IS THE PROOF, and that is deliberate: if either tool were missing
the step fails loudly here, before merge. Better than any claim in the comment.
Verified locally too: shellcheck --shell=sh --severity=error scripts/install.sh
clean, dash -n scripts/install.sh clean.
Closes#534.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tracebloc-release-train
tracebloc-release-trainBot restored the release-train/to-main branch August 20, 2026 11:32
Sign up for freeto 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.

2 participants

@LukasWodka@saqlainsyed007