Skip to content

release-train: staging -> main - #953

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

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

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Sep 1, 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 jobs-manager pod startup order, expands flag-gated Secret RBAC, and alters Windows PATH/CLI placement—high-impact areas, but heavily guarded by new drift scripts and unit tests.

Overview
This release train bundles several edge-client fixes and CI/installer hardening, with the Helm chart bumped to 1.9.92.

jobs-manager startup: A wait-for-mysql init container now runs on every install (not only hostPath), probing mysql-client:3306 with a 300s bounded Python wait using the jobs-manager image (non-root, OpenShift-safe). init-writable-data stays hostPath-only; unit tests were updated to assert by init name rather than index.

Chart rendering & RBAC: All .Values.hostPath subkey reads use (default dict .Values.hostPath) so helm upgrade --reuse-values from older releases without hostPath no longer nil-panics. With per-experiment DB creds enabled, jobs-manager gets patch on Secrets (in addition to list/delete) so cycle-2 inference can update cred secrets instead of 403ing silently.

Installers: Bash gains _dashboard_url in common.sh so credential prompts and summaries point at dev/stg/prod dashboards, not always production. Generated values.yaml is truncated and chmod 600’d before writing clientPassword, with a visible warning if the mode cannot be enforced. On Windows, the installer copies tracebloc.exe into the machine-wide tools dir (directional version refresh, no downgrade), tightens Machine PATH dedup, requires a machine-wide CLI for the fast path, and surfaces exit codes in more failure messages.

Drift/CI: DRIFT_GUARDS is one guard per line; the make drift recipe trims entries, rejects empty/|| slots, and adds guards for hostPath reads and MySQL wait behavior.

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

aptracebloc and others added 9 commits September 1, 2026 09:14
…s (backend#2910) (#939)

* fix(chart): guard every hostPath subkey read so --reuse-values renders (backend#2910)

`client/templates` read `.Values.hostPath.<key>` unguarded in nine places.
Under `helm upgrade --reuse-values` from a release predating the `hostPath`
key, `.Values.hostPath` is nil and the first such read nil-pointers, killing
the whole render (reproduced: `shared-images-pvc.yaml:3`).

Route each read through the chart's own convention `(default dict
.Values.hostPath).<key>` (already used at six other sites). The three ternary
sites also get `| default false` so the nil subkey coerces to the bool
`ternary` requires.

Add scripts/tests/hostpath-reads-guarded.sh to the required `drift` gate. It
DERIVES the site list from the tree — greps client/templates for the literal
`.Values.hostPath.`, the one signature a guarded read `(default dict
.Values.hostPath).enabled` cannot contain — so a new unguarded read is caught
without restating the nine paths (CLAUDE.md rule 1), and un-guarding any site
reddens it. Fails closed if the hostPath surface disappears.

Bump Chart.yaml 1.9.89 -> 1.9.90 (chart-version-guard).

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

* fix(chart): bump to 1.9.91 above develop after merge (backend#2910)

develop advanced to 1.9.90 (#929) while this branch was open; the identical
bump auto-merged, so the chart-version-guard saw no net change. Bump one
patch above develop.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…py it into the machine-wide tools dir (backend#2915) (#937)

* fix(installer): persist the Windows CLI dir to the Machine PATH for fresh shells (backend#2904)

The tracebloc CLI's own installer PATH-adds its bin dir at USER scope only. A
fresh, non-interactive shell that sources no profile -- the SSM session the
Windows e2e opens for its cli-windows step, which need not even run as the
installing user -- never sees that entry, so `tracebloc` is not on PATH there
even though every other client tool (installed to %ProgramFiles%\tracebloc\bin
on the MACHINE PATH) is.

After a successful CLI install, persist the CLI's bin dir onto the MACHINE PATH
through one dedup-correct helper (Add-DirToMachinePath), and route
Initialize-ToolDir through the same helper -- replacing its substring dedup,
which both false-matched a prefix dir and mis-parsed a '[' in a path, and could
append a ';;' empty (== current-directory) PATH entry. RefreshPath mirrors the
value into the running process so the verify step sees it immediately.

Pester covers the append/dedup/idempotency logic and the install wiring on Linux
CI (the .NET Machine-scope setter is a no-op off-Windows, so the registry is
simulated via mockable Get/Set-MachinePath wrappers); the self-hosted Windows
e2e exercises the real path via Initialize-ToolDir.

Part of tracebloc/backend#2904

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

* chore(installer): regen manifest.sha256 for the install-k8s.ps1 change (backend#2904)

The R8 signed-installer manifest pins a SHA256 over each sub-script the bootstrap
verifies before running privileged steps; editing install-k8s.ps1 changes its
digest, so `make drift` (gen-manifest.sh --check) fails until the manifest is
regenerated and committed.

Part of tracebloc/backend#2904

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

* test(installer): make the CLI-persist-failure test bite (backend#2904)

The persist-failure test asserted only `Should -Not -Throw`, which the
function-wide catch in Install-TraceblocCli already guarantees — so it passed even
with the load-bearing inner Add-DirToMachinePath try/catch deleted (Bugbot). Assert
the discriminating behavior instead: a persist throw must be CONTAINED so
Test-TraceblocCli still runs and the CLI is reported installed, not bounced to the
function-wide catch that misreports the successful CLI install as failed. Verified
by mutation: removing the inner try/catch now fails this test.

Part of tracebloc/backend#2904

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

* fix(installer): copy the CLI into the admin-only tools dir, not LOCALAPPDATA on the Machine PATH (backend#2915)

Review (saadqbal): adding %LOCALAPPDATA%\Programs\tracebloc to the Machine PATH is
wrong twice over. It's a per-user, user-writable dir on the system search path
(CWE-426 — a non-admin plants an exe an elevated process resolves unqualified), and
in the elevate-to-a-different-admin flow it's the admin's profile, unreadable by the
daily user, so the CLI is still unresolvable — the very case this was meant to fix.

Instead, on a successful CLI install, COPY the exe into $TOOL_DIR
(%ProgramFiles%\tracebloc\bin) — admin-only and already on the Machine PATH from
Initialize-ToolDir — via Publish-TraceblocCliToToolDir. Resolvable machine-wide for
any user, and no new PATH entry. Not a shim: a tb.cmd-style shim would bake the
admin's LOCALAPPDATA path and reintroduce the unreadable-profile problem.

Also (review): Add-DirToMachinePath's guard now rejects whitespace
([string]::IsNullOrWhiteSpace), matching Test-DirOnPath's trim — a ' ' dir no longer
slips through to append a junk PATH entry. Add-DirToMachinePath stays the shared
Machine-PATH helper for Initialize-ToolDir (exact per-entry dedup, ;;-collapse).

Manifest regenerated for the install-k8s.ps1 change.

Closes tracebloc/backend#2915

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

* fix(installer): make a failed CLI publish loud, and gate the "ready" verdict on the machine-wide copy (backend#2915)

Bugbot: Copy-Item/New-Item raise NON-terminating errors under the installer's
default $ErrorActionPreference='Continue', so the try/catch around
Publish-TraceblocCliToToolDir never caught a failed copy — and Test-TraceblocCli
could still print "ready" off the CLI installer's User-scope LOCALAPPDATA entry
even though $TOOL_DIR never received tracebloc.exe, leaving a fresh non-interactive
shell unable to resolve it.

  * Publish-TraceblocCliToToolDir: -ErrorAction Stop on Copy-Item/New-Item + a
    post-copy Test-Path, so a failure THROWS and is logged by the caller.
  * Test-TraceblocCli: the "ready" verdict now requires the MACHINE-WIDE artifact
    ($TOOL_DIR\tracebloc.exe, on the Machine PATH), not just `Has tracebloc`. When
    the CLI resolves only via the installing user's own User PATH, it says so
    honestly ("installed for you, but not machine-wide") instead of a false ready.

Tests: publish now propagates a copy failure and throws on a missing artifact;
Test-TraceblocCli asserts the machine-wide gate and the honest user-only branch.
Manifest regenerated.

Part of tracebloc/backend#2915

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

* fix(installer): gate the fast path on the machine-wide CLI copy so re-running repairs it (backend#2915)

Bugbot: Test-TraceblocCli now treats the machine-wide $TOOL_DIR\tracebloc.exe as the
"ready" condition, but the fast nothing-to-do path still gated only on
Test-TraceblocCliCurrent — which returns true for a User-PATH-only CLI. A machine
that completed a prior install with the CLI on the USER PATH only (an older
installer, or a copy that failed) would shortcut past Install-TraceblocCli and never
run Publish-TraceblocCliToToolDir, so re-running the installer — the documented
repair — left a fresh/other-user shell unable to resolve tracebloc.

Add Test-TraceblocCliMachineWide (is $TOOL_DIR\tracebloc.exe present?) and require it
in the fast-path gate alongside Test-TraceblocCliCurrent, so a completed-but-User-only
machine falls through and the copy is placed. Wiring test + unit tests added; manifest
regenerated.

Part of tracebloc/backend#2915

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

* fix(installer): keep the machine-wide CLI copy fresh so it can't shadow an update (backend#2915)

Bugbot: the $TOOL_DIR copy sits on the Machine PATH, searched BEFORE the CLI
installer's updatable %LOCALAPPDATA% copy. Once the snapshot was at/above the 0.10.0
floor the fast path never republished, so a later `irm <cli>/install.ps1 | iex` or a
CLI self-update refreshed only %LOCALAPPDATA% while `tracebloc` kept running the stale
machine snapshot.

  * Publish-TraceblocCliToToolDir now SKIPS the copy when the machine copy already
    matches the source (SHA256), and re-copies when it differs (or the compare fails).
    Being a cheap no-op when in sync makes it safe to call on every run.
  * The fast nothing-to-do path now calls Publish-TraceblocCliToToolDir among its other
    idempotent "re-run is a real remedy" repairs, so an out-of-band update to
    %LOCALAPPDATA% is picked up on the next installer run instead of being shadowed
    indefinitely.

Tests: publish skips-in-sync / re-copies-when-stale / re-copies-on-compare-error; a
wiring assertion that the fast path calls Publish. Manifest regenerated.

Part of tracebloc/backend#2915

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

* fix(installer): make the machine-copy refresh no-downgrade, and give the -ErrorAction Stop guard a biting test (backend#2915)

@LukasWodka, two non-blocking review findings:

1. The staleness refresh was direction-blind — Publish-TraceblocCliToToolDir copied
   on ANY hash difference, so a %LOCALAPPDATA% holding an OLDER build (a pinned CLI or
   a partially-failed reinstall) would let the fast path silently DOWNGRADE the
   machine-wide CLI for every user. Now directional: new Get-TraceblocExeVersion, and
   the refresh copies only when the machine copy is MISSING or the source is a strictly
   NEWER version (identical/older/unknown leaves it in place). Tests discriminate the
   downgrade case (mutation-verified: a direction-blind mutant reddens).

2. The -ErrorAction Stop guard test was inert — it mocked Copy-Item { throw }, and a
   thrown error is terminating regardless of -ErrorAction, so deleting the flag left the
   suite green. The test now emits a NON-terminating error via Write-Error, so it only
   throws when -ErrorAction Stop promotes it (mutation-verified: removing the flag
   reddens).

Manifest regenerated.

Part of tracebloc/backend#2915

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

* fix(installer): repair a broken machine CLI copy instead of refusing on an unreadable version (backend#2915)

Bugbot: the no-downgrade guard from the previous commit was too broad. When the
machine-wide tracebloc.exe exists but won't report a version (corrupt / wrong-arch),
Publish refused to replace it — and since Test-TraceblocCliMachineWide only checks
presence and Test-TraceblocCliCurrent fails open on an unreadable version, the fast
path printed nothing-to-do and re-running never repaired the corrupt snapshot that
shadows every user's CLI.

The two "unknown version" sides are NOT symmetric:
  * SOURCE version unreadable -> can't vouch for it -> keep the machine copy (a
    partially-failed reinstall must not clobber a working CLI), but
  * source READABLE, DEST version unreadable -> the machine copy is broken and the
    source is known-good -> REPAIR it (copy).
Both readable still copies only toward a strictly newer version (no downgrade).

Tests split into the two asymmetric cases (source-unreadable keeps; dest-unreadable
repairs; both-unreadable keeps); mutation-verified the repair path bites. Manifest
regenerated.

Part of tracebloc/backend#2915

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

* fix(installer): honest hint when the CLI isn't machine-wide — not "re-run as Administrator" (backend#2915)

Bugbot (Low): the "installed for you, but not machine-wide" branch told the operator
to re-run as Administrator. The installer has already self-elevated, so another run
hits the same no-op — the machine-wide copy is absent because the copy FAILED (in the
log) or a custom INSTALL_PREFIX put the CLI where Publish never sees it. Name the real
causes and point at the log instead of a fix that can't work.

Test asserts the hint no longer says "as Administrator" and names the real cause.

Part of tracebloc/backend#2915

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

* fix(installer): the machine-wide ready verdict names `tracebloc`, not the per-user `tb` (backend#2915)

Bugbot: the machine-wide "ready" branch preferred `tb`, but `Has tb` is true only
because RefreshPath pulled in the installing user's User PATH, where the CLI installer
dropped its per-user `tb.cmd` shim. We copy only tracebloc.exe into $TOOL_DIR, so a
fresh or other-user shell — exactly what this verdict is about — has `tracebloc` on the
Machine PATH but no `tb`. Naming `tb` promises a command that won't resolve in the
shell the message says works. Name the machine-wide command, `tracebloc`.

Test asserts the ready verdict says run 'tracebloc' and never run 'tb'.

Part of tracebloc/backend#2915

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ckend#2795) (#947)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…n, and check that it took (backend#2931) (#945)

* sec(installer): set the values-file mode BEFORE the credential goes in, and check that it took (backend#2931)

The generated $HOST_DATA_DIR/values.yaml holds clientId and clientPassword in
cleartext on every operator and customer machine. Two defects, same line.

ORDERING. The heredoc created the file at the PROCESS umask and the chmod ran
after its EOF -- so under any umask but 077 the file existed world-readable for
the duration of the write, with the credential already in it. A mode fixed one
line too late is not a mode. It now creates the file EMPTY and 0600 first; `>`
on an existing file truncates without touching its mode, so the heredoc writes
into a file that is already 0600. The remaining window is on a 0-byte file.

RIGHT BY ACCIDENT, AND UNTESTED. common.sh sets `umask 077`, so today the mode
happens to be 0600 and the chmod is a no-op -- but the installer does not hold
077 everywhere (_install_userspace_tools sets 022, the trap telemetry.bats
already documents for the spool), and nothing asserted the mode. The telemetry
spool has such a test; the file holding a credential had none.

FAIL-OPEN. `chmod 600 ... 2>/dev/null || true` could not fail: a chmod that did
not apply left the credential readable and said nothing. It now reads the mode
back -- POSIX `ls -ldn`, never GNU-only `stat -c`, which BSD rejects silently
(hostpath-prep.bats:181) -- and WARNS naming the mode found. Warn, not error:
chmod genuinely cannot apply on a HOST_DATA_DIR without POSIX modes (exFAT, some
WSL mounts) and refusing to install there would trade a readable file for no
tracebloc at all. An operator can act on "-rw-r--r--"; they cannot act on silence.

Only ever the file the installer GENERATES -- this branch is the `else` of the
TRACEBLOC_VALUES_FILE dev-mode override, so a caller`s own file is untouched.

tenant-config.example.env told operators to take CLIENT_ID/CLIENT_PASSWORD "from
the old release values" -- the one path the chart now tells them to stop using.
A tenant that followed values.yaml (install once, then drop them) has nothing
there, so the instruction returns empty mid-migration with nothing saying why.
Secret first, values as the documented fallback.

4 tests, all mutation-proved. Two came back GREEN first and both were real:
the end-state mode test CANNOT see the ordering defect (deleting the pre-heredoc
chmod leaves it passing, because the trailing chmod still fixes the mode before
anything reads it) -- so a separate test observes the file SIZE at the moment
chmod is called and requires the first call to see 0 bytes; and the ls-ldn
detector first matched its own comment saying "never stat -c", so it now strips
comments and asserts the mode-read line is in the extract.

bats: 244 passing vs 240 on develop = exactly the 4 new tests, 0 failures either
way. shellcheck -S warning -x clean, bash -n clean.

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

* chore(manifest): regenerate for the values-file mode change (backend#2931)

install-client-helm.sh is on the bootstrap digest-pinned surface, so changing it
without regenerating scripts/manifest.sha256 fails the required Source-of-truth
drift check -- and takes gen-manifest.bats "a clean tree passes --check" and the
Unit tests job down with it. Three red checks, one cause.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…kend#2626) (#933)

* chore(make): one drift guard per line, and refuse an empty entry (backend#2626)

The DRIFT_GUARDS list is one `|`-separated line, so it conflicted every time two
branches each added a guard - three times on client#911 in a single day.

THE OBVIOUS CLAIM IS FALSE, and I checked instead of asserting it: one-per-line
does NOT make those appends auto-merge. Appending requires editing the previous
last line to add its `|\` continuation, so both sides still touch the same line
and git still conflicts. Measured both ways on the real 38-entry list.

What changes is whether the conflict can be RESOLVED BY READING it:

  one line   conflict hunk containing 1842-character lines. Finding which guard
             each side added means diffing two 1842-char strings by eye, and the
             resolution that looks right in a diff viewer is "take one side" -
             which silently DELETES the other branch`s gate. A required check
             stops existing and every run still reports green.
  one each   the differing entry is a 30-character line. You can see it.

So this buys legibility at the moment of highest risk, not automation. The comment
in the Makefile says exactly that rather than the tidier version.

AND IT CLOSES A FAIL-OPEN the new layout makes easy to type. A `||` in the middle
yields an EMPTY entry; `sh -c ""` exits 0 and `ran` still increments, so the count
check passes and the run reports "all N guards green" with one guard being the
empty string. Measured before the fix: `DRIFT_GUARDS=true||true` printed "all 3
guards green" and exited 0. A TRAILING `|` was already caught - the `for` drops a
trailing empty field so `ran` falls short - which is why only the middle case
needed a new guard. Entries are trimmed, so a whitespace-only entry is refused
too, and `==>` lines no longer carry the leading space Make`s continuations add.

NEW: scripts/tests/drift-list-integrity.bats, 8 cases. Until now the recipe`s
three fail-open guards were asserted by a COMMENT and had been found by hand
twice. Every case drives `make drift` itself with a crafted list rather than
re-implementing the splitting, so a copy of the rule cannot go on passing while
the recipe drifts. It also fails closed if the committed list ever shrinks below
20 entries, which would leave the crafted cases green while `make drift` gated
almost nothing.

Mutation-proved - each of the four refusals is independently load-bearing:

  empty-entry guard removed   -> 6 of 8 (the `||` and whitespace cases)
  the trim removed            -> 7 of 8
  the count check removed     -> 7 of 8
  the empty-list check removed-> 7 of 8

NOT DONE, and named in the Makefile: deriving the list from `scripts/tests/*.sh`
would remove the conflict entirely and make an added guard FILE auto-merge. It is
left out because it changes which scripts RUN - a new .sh in that directory never
meant as a drift guard would start gating merges - so it needs its own change and
its own opt-out list.

VERIFIED: make drift 38/38 green; CI=true bats scripts/tests/*.bats 1603 passing,
0 failures; bats-hygiene green; shellcheck -S warning -x clean; manifest --check
clean. Makefile only - no chart content, so no version bump.

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

* test(make): the list-size floor accepts both shapes Make can print (Bugbot)

DEMOTED, THEN HARDENED ANYWAY. The finding said GNU Make 4.x renders an exported
simply-expanded variable as `export DRIFT_GUARDS :=`, so a `^DRIFT_GUARDS :=`
matcher misses on Ubuntu CI and the floor never counts. Measured: this suite
PASSED on Ubuntu CI as written - `bats (bash unit, mocked) = SUCCESS` on the very
platform named - because Make records the assignment and the `export` directive
separately. So it does not reproduce, and a miss would have failed CLOSED (the
`-n` check) rather than passing silently.

But the matcher WAS depending on which of two shapes a given Make emits, and one
alternation removes that dependency. Local make here is 3.81; CI`s is 4.x; the
test should not care.

AND THE OTHER SHAPE IS NOW TESTED DIRECTLY, which is the part that would have
stayed uncovered: the committed-list assertion can only ever exercise the shape
THIS Make emits, so the alternative branch would have been untested until a Make
upgrade made it live - exactly the gap the finding pointed at. All four forms are
driven (with and without `export`, `:=` and `=`), plus a CONTROL that a line
merely MENTIONING the name is not an assignment.

Mutation-proved:

  the optional export prefix dropped   -> 8 of 9 (the reported shape)
  the matcher made over-broad          -> 8 of 9 (the control bites)

make drift 38/38; bats-hygiene green; shellcheck -S warning -x clean; manifest
--check clean.

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

* test(make): the shape case must reach the floor matcher, not a copy of it (backend#2868)

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

* test(make): return the VALUE so the prefix strip is testable (backend#2868)

Built on 5bf6002 rather than beside it - a co-driver and I fixed the same Bugbot
finding on this branch at the same time, and 5bf6002 got there first. Its
one-matcher-two-callers shape is kept; this is the delta.

THE COUNT MADE THE STRIP UNTESTABLE. `awk -F"|" NF` returns the same number
whether or not the assignment prefix was removed, because a leftover prefix just
rides along inside field 1. Measured on 5bf6002 itself: narrowing the `sed` to
`^DRIFT_GUARDS := ` left all NINE cases green. So the half of the matcher the
finding was actually about - handling both shapes Make can print - was still only
half covered.

The function returns the value; callers count; the shape case asserts it
BYTE-EXACT, which is the only thing a wrong strip changes.

AND THE RENAME EXPOSED A VACUOUS CONTROL, which is the part worth reading. The
control was `! printf ... | drift_guards_entries`. Renaming the function left
that line pointing at nothing - `! missing_command` is non-zero, `!` inverts it,
and the control went on passing while exercising NOTHING. Routing it through
`bash -c` to capture the status made it worse: a bats-defined function is invisible
there, so every run was 127. It now asserts the function EXISTS (`type -t`) and
THEN that it refuses, so "it refused" can never be satisfied by "it is gone"
(CLAUDE.md rule 10).

Mutation-proved, all three reaching the live function:

  the sed narrowed          -> red  (the weakness this commit closes)
  the function renamed away -> red on TWO cases (the vacuous control, now caught)
  the grep narrowed         -> red  (the original finding shape, still covered)

make drift 38/38; bats-hygiene green; shellcheck -S warning -x clean.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ard the class (backend#2906) (#938)

* fix(installer): name the exit code when the CLI install fails (backend#2906)

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

* fix(guard): derive the exit-code gate spelling, and name the code on the k3d path (backend#2906)

Bugbot, Medium: the AST sweep matched only the dotted `.ExitCode -eq/-ne 0`
spelling, so the house idiom after a wait -- copy the code into a local, branch
on the local -- was invisible to it. `$k3dExitCode = $k3dProc.ExitCode` …
`if ($k3dExitCode -ne 0) { Err … }` is exactly that, and it reported a k3d
failure with no code while this guard called the class closed. Restating one
spelling is what went wrong in client#913; restating two is the same mistake
with a longer list. The variable names are now DERIVED from assignments whose
right side reads `.ExitCode`.

Compliance follows ONE HOP through the call arguments, which is what separates
the two sites this newly sees. The GPU branch names no code in its Warn text,
but $GPU_SKIP_REASON is assigned in that branch from Get-GpuBuildFailureReason
-ExitCode $buildExit, whose fallback returns "docker build exit $ExitCode" --
the classifier is deliberately preferred over a bare number and the comment
there says so. The k3d branch has no such hop: the code is read, tested and
dropped. Fixed with the house Format-ExitCode helper, matching the sibling site
at :4086.

Mutation-proved, anchors asserted both times:
  revert the k3d fix                          -> 1 of 2 reddens
  -ExitCode $buildExit -> -ExitCode 0          -> 1 of 2 reddens
The second mutation initially did NOT redden: the token regex matched the bare
word ExitCode, which the PARAMETER NAME satisfies with a constant value, so the
one-hop was a blanket pass. Tightened to the property read and the gate variable.

Not widened to $LASTEXITCODE here: 28 uses, and the widening surfaces 7 further
offenders. That is a red gate on arrival, so it is filed separately rather than
landed (CLAUDE.md rule 4 -- arm while green).

Pester: 868 total, 853 passed, 0 failed. make drift: all 38 guards green.

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

* fix(guard): the gate misses $LASTEXITCODE, the commonest spelling (backend#2906)

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

* fix(guard): a stale exit code is worse than none, so do not demand one (backend#2906)

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

* fix(guard): follow BOTH exit-code spellings, and the site that exposed (backend#2906)

@saadqbal's change-request, and he was right that the floor passing at 8 is what
made the hole self-concealing.

THE DERIVATION FOLLOWED ONE SPELLING. `$a.Right.Extent.Text -match '\.ExitCode'`
never matched `$createRc = $LASTEXITCODE`, so `$createRc` never became a gate token
and `if ($createRc -ne 0)` at install-k8s.ps1:5787 was invisible to the walk.
`$LASTEXITCODE` had been added as a DIRECT token, which catches
`if ($LASTEXITCODE -ne 0)` but not the copy-into-a-local idiom -- the same shape
round one flagged for `.ExitCode`, fixed for one spelling. Widened to
`'\.ExitCode|\$LASTEXITCODE'`, floor 8 -> 9.

AND WIDENING IT IMMEDIATELY NAMED A LIVE SITE, which is the point:

  line 5789 gated on [$createRc -ne 0]: Err "Couldn't provision the client.
  Re-run to retry.", but got 1.

Exactly the case he described -- `Print-CreateFailure` receives `-OutFile` and
`-Location`, never the code, so an operator got "The client couldn't be
provisioned." followed by "Couldn't provision the client. Re-run to retry." with
no code at all. Fixed with the house pattern:

  Err "Couldn't provision the client (tracebloc exited $(Format-ExitCode
  $createRc)). Re-run to retry."

So the two-line change is a three-line change: the guard widening is what makes
the product fix findable, and leaving the floor at 8 would have let both sit.

Pester: 853 passed, 0 failed, 15 skipped. drift 38/38. Manifest regenerated
(install-k8s.ps1 moved).

A note on my own process, since it bit me twice here: two earlier edits in this
pass were silently lost because a later anchor miss in the same script aborted
before `write_text`. Both edits are now applied one at a time and each re-read
from disk to confirm it landed -- the widening in particular reported "ok" once
without ever being written, and the Pester run that followed was measuring the
unmodified file.

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

* fix(guard): the THIRD spelling, and a guard that could not be satisfied (backend#2906)

Bugbot Medium: the gate derived from `.ExitCode` and `$LASTEXITCODE` only, so
branches on `.Code` were invisible. `.Code` is the HOUSE result shape --
Invoke-BoundedProcess and Invoke-DockerCli return @{ Code; Output }, documented at
install-k8s.ps1:2307-2308 -- so this was the commonest wrapper spelling, not an
edge case.

DERIVED, NOT COUNTED BY HAND. Bugbot named 2 sites. Widening the gate and letting
the walk report found 31 `.Code` gates in the file, 7 of them carrying a
user-facing Warn/Err. The finding was 2 of 7.

TWO OF THE 7 ARE DISJUNCTIONS AND ARE NOW EXCLUDED, for the same reason the ELSE
and the CATCH already are: the guard cannot know WHICH disjunct fired, so
demanding the code demands a possibly-false cause. `if ($res.Code -ne 0 -or $out
-match "FAIL " -or $unconfirmed.Count -gt 0)` fails on a non-code disjunct with a
code of 0, and "exit 0" beside a failure is the wrong-cause-reads-as-information
outcome this Describe exists to refuse. @saadqbal flagged this hazard
pre-emptively for the bool-collapsed sites; it arrived here first.

THE FIVE REAL ONES NOW NAME THE CODE. All five are wrapper calls where 124 IS the
timeout, so the code is precisely what separates the two causes the old text made
the operator guess between -- ":1534 nvidia-smi failed or timed out" being the
clearest case.

AND THE GUARD WAS UNSATISFIABLE FOR ONE ITERATION, which is the finding worth
reading. Widening the GATE alone left compliance matching `.ExitCode` only: all
five sites kept failing with the code sitting in the message text. A guard that
cannot be satisfied is worse than one that does not check -- it trains the reader
to edit the guard instead of the code. The two sides now widen in one place, named
as such.

Floor 9 -> 14, MEASURED by raising it until it failed and reading the number back,
not by counting additions by hand -- which is how a floor and a walk start
disagreeing.

Base merged (#937 had landed; conflict was scripts/manifest.sha256 alone) and the
manifest REGENERATED rather than resolved by picking a side, per @saadqbal.

4 mutations, all reddening: drop the code from a fixed message; narrow the gate
back; narrow compliance back (the unsatisfiable state); drop the disjunction
exclusion. Full Pester suite: 1061 passed, 0 failed. gen-manifest --check clean.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…e too (backend#2849) (#946)

* fix(installer): the dashboard link follows CLIENT_ENV on the bash side too (backend#2849)

Bugbot on client#931: `Get-TraceblocDashboardUrl` fixed the Windows half,
and the bash twins still hardcoded https://ai.tracebloc.io at TEN sites
across install-client-helm.sh and summary.sh -- while `_backend_url`
directly above was correctly env-aware. So a `CLIENT_ENV=dev` install on
Linux or macOS still sent the operator to the PRODUCTION dashboard for
credentials that dev-api then rejects. Same defect, other half of the
contract -- and exactly the "fix landed in one twin only" rule this week
added to .cursor/BUGBOT.md.

`_dashboard_url` mirrors the PowerShell helper: hosts taken from the
backend's OWN per-environment settings (DEVICE_VERIFICATION_URI /
RESET_PASSWORD_URL in xraybackend/settings/{dev,stg,prod}.py) rather than
guessed, and the SAME `tb_client_env` alias reduction plus unknown->prod
fallback as `_backend_url`, so the two can never disagree about which
environment an install belongs to.

Verified against the REAL tb_client_env, not a stub: dev/development ->
dev.tracebloc.io, staging/stg -> stg.tracebloc.io, production/unset/
unknown -> ai.tracebloc.io, plus the path and bare-host forms. My first
check used a `${1,,}` stub that silently failed on macOS bash 3.2 and made
every case look like prod -- the harness was wrong, not the code. The
helper itself is bash-3.2 clean (no case conversion, no ${var,,}).

7 bats guards, including one pairing `_dashboard_url` with `_backend_url`
per environment (the defect WAS those two disagreeing) and one asserting
no live link in either file carries a hardcoded prod path. 247 bats pass;
bats-hygiene clean (every assertion enforces); shellcheck --severity=error
clean.

Part of tracebloc/backend#2849.

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

* fix(installer): _dashboard_url belongs in common.sh, not one of two siblings (client#946)

@LukasWodka, and the bats failure was real. I defined the helper in
install-client-helm.sh and CALLED it from summary.sh at four sites --
sibling libraries where neither sources the other -- so summary.sh
silently depended on a load order nothing enforces.

Loaded standalone (exactly what summary.bats does) those four rendered an
EMPTY link: "See it on your dashboard:" followed by nothing, and the
"Create a use case" line lost its link too. That is WORSE than the bug
being fixed: a wrong URL a reader can recover from, a missing one they
cannot.

And it is the same defect class this PR exists to close, one layer down --
a fix that landed in one twin only, reproduced between install-client-helm.sh
and summary.sh while fixing it between bash and PowerShell.

common.sh is the fix Lukas named and it is right: install-k8s.sh sources
it first, the bats `load_lib` sources it first, and it already holds
`tb_client_env` -- which this calls. Both twins get it by construction
rather than by accident. Sourcing summary.sh from the helm lib would have
worked too and re-created the coupling in the other direction.

Tests follow the helper into common.bats, plus two new guards for the
defect itself: the helper is reachable from summary.sh loaded STANDALONE,
and no lib under scripts/lib defines it except common.sh. The
_backend_url pairing test stays in install-client-helm.bats -- it is the
one assertion needing both helpers in scope, and the defect it guards is
those two disagreeing.

summary.bats and copy-catalog.bats are green again; bats-hygiene clean;
shellcheck --severity=error clean on all three libs.

Part of tracebloc/backend#2849.

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

* test(installer): pin CLIENT_ENV in the copy catalog, which now depends on it (Bugbot on #946)

`print_summary` renders `_dashboard_url`, which maps dev|stg|prod to three
different hosts -- so the 01-outcomes golden now varies with CLIENT_ENV,
and `copy-catalog.bats` setup() pinned every other drift source but not
that one.

Reproduced before fixing rather than inferred:

    CLIENT_ENV=dev bats scripts/tests/copy-catalog.bats
    not ok 2 installer copy catalog: 01-outcomes is current

Two consequences, and the second is the worse one: a developer whose shell
carries CLIENT_ENV=dev fails the catalog for no reason of their own, and
regenerating the golden from that shell would bake dev.tracebloc.io into
the committed copy a CUSTOMER reads.

Pinned to prod, which is what the golden is FOR -- the customer-facing
copy -- and which `_dashboard_url` already treats unset as. The pin makes
that explicit instead of dependent on the developer's environment being
clean, matching the NO_COLOR and TRACEBLOC_BANNER_SHOWN pins beside it.

Checked the sibling suites for the same exposure: summary.bats embeds no
host literals, so it is genuinely env-agnostic. All four suites now pass
under a deliberately dirty CLIENT_ENV=dev shell -- copy-catalog 2,
summary 30, common 101, install-client-helm 241, zero failures.

Part of tracebloc/backend#2849.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…942)

* fix(chart): wait for MySQL before jobs-manager starts (backend#2913)

jobs-manager exits on its first failed DB connection, so a MySQL a few
seconds behind it crashloops the container. initContainers was gated on
hostPath.enabled, so an edge without it had no init containers at all;
the gate now covers only init-writable-data.

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

* test(chart): assert the init containers by NAME, and arm the guard that gates them (backend#2913)

Two Bugbot findings, both real, and both about position rather than about the
change itself.

1. HIGH -- THE REQUIRED HELM SUITE ASSERTED ORDINAL SLOTS. `wait-for-mysql`
   becoming `initContainers[0]` moved `init-writable-data` to `[1]`, so the #611
   and #672 cases were inspecting the WRONG CONTAINER while still passing their
   own names, and the CSI case asserted `notExists: initContainers` -- true of the
   chart until this PR, false after it. Measured before fixing: 3 failed / 640
   passed.

   NOT renumbered to `[1]`, because that re-couples the test to ordinal position
   and is what broke. 23 paths now select by name --
   `initContainers[?(@.name=="init-writable-data")]` -- the filter form this repo
   already uses for `volumes` and `env`. The `[0].name == init-writable-data`
   equality became `exists:` on the filter, since under a name filter the old
   assertion was tautological.

   The CSI case now states the property its TITLE always claimed -- "skips the
   privileged init", not "has no inits": no `init-writable-data`, nothing running
   as `runAsUser: 0`, and -- for non-vacuity -- `wait-for-mysql` present, so both
   refusals are about a populated list rather than an empty one.

2. MEDIUM -- THE NEW GUARD WAS NOT ON THE LIST THE GATE RUNS.
   `jobs-manager-waits-for-mysql.sh` existed and passed, but `DRIFT_GUARDS` did
   not name it, so `make drift` and the required Source-of-truth drift job never
   invoked it: advice, not a gate. Now armed, 38 -> 39 guards.

   ARMED WHILE GREEN: the guard was run standalone first (rc 0) before being added,
   rather than landing a red gate.

   Mutation-proved with the regression the finding names -- folding `wait-for-mysql`
   back under `hostPath.enabled`, which leaves a VALID chart -- and `make drift`
   now fails with `FAIL hostPath.enabled=false: first initContainer is 'api', want
   'wait-for-mysql'`. My first attempt deleted the container outright, broke the
   template, and produced a render error instead of the guard's refusal; that
   proved nothing about the guard and is not what is recorded here.

helm unittest 643/643, drift 39/39, check-facts 14/14, shellcheck + bash -n clean.
Chart already one patch above develop (1.9.91 vs 1.9.90); manifest unchanged, since
no installer script moved.

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

* fix(chart): do not pin a uid on the arbitrary-UID path (backend#2913)

OpenShift restricted assigns a uid from the project range; one pinned
container fails SCC admission for the whole pod. runAsNonRoot is inherited
from the pod, so nothing is loosened.

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

* fix(chart): use the app image for the wait, so no uid needs pinning anywhere (backend#2913)

Bugbot High, and it independently confirms the plain-Kubernetes regression I raised
on this PR last round: dropping the pin fixed OpenShift and broke every other
platform.

THE MECHANISM. `runAsNonRoot: true` only FORBIDS uid 0 -- it assigns nothing -- so
with no `runAsUser` anywhere (which the OpenShift arbitrary-UID path requires) the
kubelet reads the effective user from the IMAGE. `library/busybox` declares
`Config.User` EMPTY (measured: `[]`), i.e. root, so AKS, EKS, k3d and bare metal
all fail the init with "container has runAsNonRoot and image will run as root".

THE FIX IS THE IMAGE, NOT A VALUES SEAM. `Dockerfile.jobs_manager:26` declares
`USER 1001` -- which is exactly why `api` and `pods-monitor-container` need no pin
either. Reusing it means NO uid is pinned anywhere in this pod: OpenShift assigns
from the project range, plain Kubernetes gets 1001 from the image, and the
arbitrary-UID invariant this branch added holds with no operator action and no new
values key. I had a `pinRunAsUser` seam built and this is strictly better -- it
needs nothing from the operator and keeps the guard exactly as written.

It also removes an image: busybox was a second pull on every edge for a TCP connect
the app image can already do. (`library/busybox` is still used by
`mysql-deployment.yaml` and by `init-writable-data`, so the values key stays live.)

THE PROBE IS PYTHON NOW, because the app image carries no netcat.
`socket.create_connection` is the same test with a real per-attempt timeout, which
`nc -z` did not have. Every message is byte-identical -- the diagnosis was already
right. Driven against a real socket, not just rendered: rc=0 with a listener, rc=1
without, and the deadline is checked AFTER an attempt so a wait starting inside the
last two seconds still gets one try.

Verified on the render: pod `runAsNonRoot=true`, and `runAsUser=<none>` on all
three containers.

Two mutations, both with the anchor asserted on disk first:
  * busybox restored          -> the new helm-unittest case fails
  * a uid re-pinned           -> the case fails AND the drift guard fires

helm unittest 644/644, drift 39/39. Chart already one patch above develop; manifest
regenerated.

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

* chore(chart): bump to 1.9.92 -- develop reached 1.9.91 (backend#2913)

The version-bump gate needs one patch above develop, and develop moved to
1.9.91 while this branch sat at 1.9.91 too. Caught on the push I had just made,
not by CI.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka
LukasWodka requested a review from saadqbal as a code owner September 1, 2026 11:27
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@waqaskhanroghani
waqaskhanroghani removed the request for review from saadqbal September 1, 2026 11:28

@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 079ab7c. 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 1, 2026
@tracebloc-release-train
tracebloc-release-train Bot merged commit 11e445a into main Sep 1, 2026
97 of 104 checks passed
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-main branch September 1, 2026 13:29
@tracebloc-release-train
tracebloc-release-train Bot restored the release-train/to-main branch September 1, 2026 14:08
@tracebloc-release-train
tracebloc-release-train Bot deleted the release-train/to-main branch September 1, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants