Skip to content

release-train: staging -> main - #609

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

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

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed release-train/to-main branch (a mirror of staging), so it never collides with a human PR. Merged only when the fr-gate is green.


Note

High Risk
Large blast radius: default-off chart knobs still change image resolution and jobs-manager spawn env when mirrors are enabled, and installer/bootstrap changes affect privileged customer install paths plus supply-chain verification behavior.

Overview
This promotion moves client chart 1.9.12 → 1.9.15 and folds in a wide set of staging changes across the Helm chart, installers, CI, and docs.

Helm / runtime: Adds global.imageRegistry so one mirror host re-homes tracebloc images, spawned ingestor/training jobs, and utility images (tracebloc.mirrorPrefix, updated tracebloc.image registry args, JOB_IMAGE_HOST / ingestor repo logic). Introduces opt-in perDatasetPvcs (PER_DATASET_PVCS on jobs-manager, cluster-scoped PV/PVC RBAC, render failure when clusterScope: false). Fixes requests-proxy to use IfNotPresent when a digest is pinned. Helm unit tests cover mirror precedence and the new flags.

Installers (bash / PowerShell): Wire TRACEBLOC_IMAGE_REGISTRY (and optional pull-secret envs) into generated values; add corporate-CA trust for host tools, offline cosign --bundle verification (#584), curated PII-free logs, top-level fatal/interrupt handling, resilient multi-transport downloads with size checks, richer preflight (network profile, blocked-registry hints), and GPU plugin deploy/verify gating so failures stay CPU-mode non-fatal. Bash gains wire_ca_trust, _assert_download_size, and a pipefail-safe _extract_yaml_value.

CI / release: GitHub Actions pins third-party actions to commit SHAs; helm-ci adds full-seal-e2e (push/dispatch only, skips until e2e secrets), workflow_dispatch, bounded Docker Hub pulls in distro matrices, and kubeconform download timeouts. Release workflow publishes and attaches manifest.sha256.bundle for offline Sigstore verify.

Docs / hygiene: docs/INSTALL.md documents mirror/air-gap installs; docs/SEAL-CHECK.md describes CI seal coverage; Bugbot notes on secretless reusable workflows; style guard for lowercase tracebloc in user-facing copy.

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

shujaatTracebloc and others added 30 commits August 4, 2026 10:10
#552) (#570)

requests-proxy hardcoded `imagePullPolicy: Always` while its image line already
read `images.requestsProxy.digest` — so a pinned digest was ignored for the pull
policy and every restart re-pulled the image even when it was already cached.
Make it digest-aware, matching jobs-manager / pods-monitor / resource-monitor:
  digest set   -> repo@digest + IfNotPresent (restart-safe offline)
  digest empty -> repo:tag    + Always (unchanged default)

+2 helm-unittest cases (default -> Always; digest -> repo@digest + IfNotPresent).
Chart 1.9.11 -> 1.9.12 (chart-content change requires a version bump).

Scope: this is the narrow, unambiguous part of #552. The broader control-plane
offline-restart update-model change (jobs-manager/pods-monitor rely on Always for
the image-refresh CronJob; resource-monitor too) is a deliberate design decision
tracked in #569.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…end#1497) (#580)

Two unbounded external calls (Bugbot on the client#571 prod promotion) that can
hang a CI job to the GitHub Actions cap with no useful failure — same class as
the image-refresh CronJob fix (#572).

- scripts/tests/e2e-seal-check.sh: add --request-timeout=10s to the point-in-time
  kubectl API calls (get/run/logs/delete/describe) so a wedged API server fails
  the seal-check fast. `kubectl wait` (L57) is deliberately left as-is: it is
  already bounded by --timeout=180s, and a --request-timeout on its underlying
  watch would truncate it and risk flakes.
- .github/workflows/helm-ci.yaml: add --connect-timeout 15 --max-time 120 to the
  pinned kubeconform download so a stalled endpoint fails the template matrix
  instead of hanging. Retries/-f behaviour unchanged.

Values mirror the repo's existing bounds (--request-timeout=5s/10s elsewhere;
--connect-timeout/--max-time in install.sh / setup-linux.sh). Neither file is in
the R8 signed manifest (test script + workflow), so no manifest bump.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…utput (#576) (#579)

* fix(installer): stop leaking user PII + tracebloc internals in logs/output (#576)

Root-caused from a client's shared install log, which exposed both her identity
(Username/RunAs/Machine — the PowerShell transcript header) and our internals
(the `& $cosign @cosignArgs` source line + internal codes) when cosign errored.

Main installer (install-k8s.ps1):
- Drop Start-Transcript entirely — its fixed header IS the PII, and it also
  captured PowerShell's raw error rendering (source lines, internal identifiers)
  into the log. The shareable install-*.log is now written only via the curated
  Log() writer, with a PII-free header.
- Route the message helpers (Info/Ok/Warn/Err/Step/PromptHeader/Hint) through
  Log() so the log stays useful without the transcript — it mirrors the curated
  on-screen output: no user PII, no tracebloc internals. Drop the Stop-Transcript
  calls.

Bootstrap (install.ps1):
- Capture cosign's output (2>&1 | Out-Null under EAP=Continue) instead of letting
  a native-stderr NativeCommandError dump THIS script's source line + internal
  identifiers to the console / any user transcript. Sanitize the verification-
  failure messages to plain language (no RFC-0001 R8 / manifest.sha256 / $_).

Bash (common.sh): drop the username from the HOST_DATASET_DIR "not writable"
error (keep the uid number).

Tests: +2 Pester (main: no transcript; helpers feed the curated log) and +3
Pester (bootstrap: cosign output captured, messages carry no internal codes,
still fails closed). Both suites green (418/0/9). Manifest regenerated.

Scope: the top-level error boundary that stops PowerShell rendering ANY unhandled
throw raw is #577; a broader sweep of internal identifiers out of all remaining
messages continues under #576.

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

* fix(installer): route preflight + summary failures to the curated log (Bugbot #579)

Removing Start-Transcript (for #576) left Write-PfFail and a few summary failure
headlines screen-only, so a forwarded install-*.log no longer showed the actual
failing preflight checks (disk/virt/storage/connectivity/memory) — only the
generic Err summary + hints. Route Write-PfFail and the bad_creds / image_pull_ca
/ diagnostics-archive failure headlines through Log().

+1 Pester (Write-PfFail reaches the log). Manifest regenerated.

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

* fix(installer): log the classified final state for every summary branch (Bugbot #579)

The default Print-Summary branch (image_pull / crash / other non-ready) still
printed its failure headline via Write-Host only, so those outcomes missed the
curated log after the Start-Transcript removal — while bad_creds / image_pull_ca
were routed. Add a central `Log "Final client state: <state>"` before the switch
so EVERY branch is covered, and route the default headline too.

+1 Pester (Print-Summary records the final state). Manifest regenerated.

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

* fix(bootstrap): sanitize the bash cosign-failure message too (#576 review parity)

Reviewer (saadqbal) on #579: install.ps1's cosign message was sanitized but the
bash bootstrap still printed 'cosign signature verification FAILED for
manifest.sha256 — refusing to install', leaking the same internals. Match the
PowerShell wording: 'Couldn't confirm the installer download is authentic, so the
install stopped before changing anything on your machine.' Same for the success
line ('Download verified as published by tracebloc').

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

* test(bootstrap): expect the sanitized cosign-failure message (#576 review)

install.sh's cosign-failure message was sanitized in 2aa35db (reviewer parity),
so install-bootstrap.bats's 'cosign signature failure aborts' test — which pinned
the old 'signature verification FAILED' string — must expect the new plain
wording. Behaviour coverage (aborts + never degrades to a same-channel sha256)
is unchanged.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…rd (#586) (#587)

House-style alignment (from the #576 review): the product name is lowercase
`tracebloc` always, and the installers must feel one-to-one across platforms
(bash is the gold standard). PowerShell had capital-T "Tracebloc" in two
user-facing lines while the rest of the copy is lowercase.

- install.ps1: "Downloading Tracebloc client installer" / "Running Tracebloc
  environment setup" -> lowercase `tracebloc`.
- check-style.sh: new guard fails on capital-T `Tracebloc` in user-facing text so
  the casing can't drift again (same spirit as the #435 single-source facts guard).
  Exempts comments + PascalCase identifiers (Get-Tracebloc… , the
  TraceblocInstallerResume resume key); honours `# style-guard: allow`.

Verified: check-style.sh clean; the guard flags a real "Tracebloc client" line and
excludes identifiers/comments; shellcheck + PS parse clean.

Closes #586.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Set-Content/Add-Content default to PS 5.1's ANSI encoding, corrupting non-ASCII
host paths/messages in the -Diagnose log; every other writer in this file already
pins -Encoding UTF8. (Bugbot on the staging promotion, backend#1497 sibling.)
…th a raw stack (#577) (#588)

* fix(installer): top-level error boundary so PowerShell never crashes with a stack (#577)

The main installer (install-k8s.ps1) had NO top-level try/catch, so any unhandled
throw (or PS runtime error) rendered PowerShell's raw source line + stack and
terminated the session — exactly Anubha's screen. Wrap the whole main run (inside
the TB_PESTER guard) in a top-level try/catch that routes any crash through a new
Show-FatalError: one clean "Installation stopped" line + the reason (the exception
MESSAGE, curated at the throw sites per #576 — never the stack) + the log location
+ a safe-to-re-run hint, then exit 1. Intentional exits (fast-path, Err, final)
pass straight through — verified exit-in-try is not caught while throw-in-try is.
Stack traces are never shown OR logged (no internals).

Bootstrap (install.ps1) already had a top-level try/catch; enhance its message to
the same clean "Installation stopped … safe to re-run" shape.

+3 Pester (main wrapped in try/catch -> Show-FatalError; clean render with reason
+ re-run hint and no stack/source; reason logged, stack never). Manifest regenerated.

Built on #576 (needs its no-transcript / curated Log world); PR opens once #576
lands so it can target a clean develop.

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

* fix(installer): guaranteed finally + trap for the PS error boundary (#577)

Completes the PowerShell side of #577 to Lukas's acceptance. The top-level
boundary now has a guaranteed `finally` + a last-resort `trap`, mirroring bash's
exit-code-guarded install_cleanup:
- $script:OutcomeReported is set on every terminal path (normal finish, Err,
  caught crash via Show-FatalError, fast-path, help/diagnose).
- The finally shows a clean "interrupted" line (Show-Interrupted: log + re-run,
  no stack) ONLY when nothing reported an outcome — i.e. Ctrl-C / abnormal
  termination — so the window never just vanishes.
- The trap catches anything that terminates OUTSIDE the try (defined inside the
  TB_PESTER guard so it never fires under the test dot-source).

Verified: reported exits stay quiet, crashes route through Show-FatalError, an
unreported exit fires Show-Interrupted; exit codes preserved.

+3 Pester. Manifest regenerated. Bash symmetry (capture a failing tool's raw
stderr -> curated line before the closer) is next.

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

* fix(installer): bash graceful failure — capture direct native-call stderr (#577)

Completes #577's bash symmetry. The big steps already curate (spin_cmd_bounded
captures tool output to the log + shows a clean ✖ line) and install_cleanup is a
clean conditional closer; the residual raw-stderr-before-the-closer leaks were a
few DIRECT native calls under `set -euo pipefail`:
- cluster.sh: `k3d cluster start` (reuse path) now redirects to the log + surfaces
  a curated error() on failure.
- gpu-plugins.sh: `kubectl apply` (device-plugin manifest) and `kubectl rollout
  status` now redirect to the log, so only the caller's curated error/warn shows.

A failing tool now surfaces our plain-language line, not its raw stderr, before the
closer — bash graceful *failure*, not just a graceful closer. Manifest regenerated.

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

* fix(installer): GPU device-plugin failure is recoverable, not fatal (#577)

Per #577's fatal-vs-recoverable acceptance (optional steps warn + continue): a GPU
device-plugin download/apply failure was fatal on BOTH platforms —
gpu-plugins.sh `error "Failed to enable GPU acceleration"` and install-k8s.ps1
`Err "Failed to enable GPU acceleration"` both exit — so a GPU hiccup aborted the
whole install instead of falling back to CPU mode (which the client fully supports).

Now both warn + continue in CPU mode:
- bash: `_apply_remote_manifest` failure -> warn + return 0.
- PS: wrap the download/apply in a catch so a failure can't reach the top-level
  boundary; warn + continue; also capture `kubectl apply` raw stderr (#577).

Mirrors the NVIDIA-container-toolkit timeout, which already warns and carries on.
+1 Pester. Manifest regenerated.

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

* fix(installer): gate GPU success message on kubectl exit code (#577)

Bugbot (High): the GPU device-plugin apply/rollout discarded output into $null and
never checked $LASTEXITCODE. Because a native kubectl non-zero exit does not throw,
a failed apply fell through to Ok "GPU acceleration enabled." - a false success the
operator would trust, and (unlike bash) the failure was neither warned nor logged.

Capture each kubectl call's output to the install log and gate the success message
on $LASTEXITCODE; on any non-zero exit, warn + continue in CPU mode (mirrors the
bash gpu-plugins.sh path). Adds a regression test asserting the success message is
exit-code-gated and the false-success $null-discard pattern is gone.

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

* fix(installer): reboot-pending stop marks outcome reported (#577)

Bugbot (Medium): the reboot-pending path prints reboot/resume guidance and exits 2,
but never set $script:OutcomeReported. The new top-level finally then treated that
intentional stop as an interruption and appended Show-Interrupted, so on a common
Step 1 path the operator saw a contradictory second outcome.

Set $script:OutcomeReported before the reboot block's exits (covers both exit 2
paths and the Restart-Computer path), so the finally leaves the clean reboot
guidance as the sole outcome. Adds a regression test.

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

* fix(installer): bound k3d start + GPU apply so log-redirected calls can't hang (#577)

Bugbot flagged two hang risks introduced by the #577 log redirects: piping a call's
output to the log hides console progress, so an unbounded call now hangs silently
instead of failing into the new curated error/warn paths.

- cluster.sh: `k3d cluster start` waits for the server with no deadline by default;
  add `--wait --timeout 5m` (parity with the Windows installer's 5-minute start
  deadline) so a wedged Docker fails into the curated error instead of hanging.
- gpu-plugins.sh: the shared `_apply_remote_manifest` (nvidia + amd) now bounds
  `kubectl apply` with `--request-timeout=30s`, mirroring the node-probe in the same
  file, so a wedged API server falls through to the recoverable CPU-mode warn.
- install-k8s.ps1: same `--request-timeout=30s` on the PS GPU apply for parity (the
  PS `k3d cluster start` is already bounded by Wait-ProcessWithDeadline).

Adds regression tests: cluster start is --timeout-bounded (bats), the GPU apply
carries --request-timeout (bats + Pester).

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

* fix(installer): gate bash GPU success on rollout + fix -Diagnose interrupt gap (#577)

Bugbot round 4:

- gpu-plugins.sh (Medium): after a non-zero `kubectl rollout status`, bash still
  printed success "GPU acceleration enabled." while the failure went only to the log
  - the false-success bug already fixed on the PS side, still present in bash. Gate
  the success on the rollout exit code (nvidia AND amd paths); on failure warn +
  continue in CPU mode, matching the PS peer.
- install-k8s.ps1 (Low): $script:OutcomeReported was set to $true BEFORE the long
  Invoke-DiagnoseBundle ran, so an interrupt mid-collection skipped Show-Interrupted
  - the silent death the boundary exists to prevent. Set the flag AFTER the bundle
  completes.

Adds regression tests: nvidia success is rollout-gated (bats); the -Diagnose flag is
set only after the bundle (Pester).

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

* fix(installer): skip GPU verify when the plugin deploy failed / CPU-mode (#577)

Bugbot round 5 (Medium): making the GPU device-plugin step recoverable (#577) means a
failed apply/download now continues the install - but the caller still ran the GPU
verify step unconditionally. Operators saw "continuing in CPU mode", then waited ~90s
and got a contradictory "still initializing" warning for a plugin never deployed.

Have the deploy signal whether the plugin was actually deployed, and gate verify on it:
- gpu-plugins.sh: _deploy_nvidia_plugin / _deploy_amd_plugin return non-zero on every
  CPU-mode path (apply failure, unconfirmed rollout, master-fallback failure); 0 when
  deployed/already-present.
- install-k8s.sh: `if deploy_gpu_device_plugin; then verify_gpu; fi` (the `if` also
  keeps a non-zero deploy from tripping set -e).
- install-k8s.ps1: Install-GpuDevicePlugin returns $true/$false (Invoke-WithRetry sunk
  to $null to avoid return-value pollution); caller runs Confirm-GpuNode only on $true.

Adds regression tests: verify is gated (bats + Pester), and a functional bats test that
_deploy_nvidia_plugin returns non-zero on a CPU-mode path.

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

* fix(installer): close the GPU "can't hang" parity + gate the amd master fallback (#577)

Review follow-ups (saadqbal) on the GPU path:

- Bound the last unbounded kubectl probes so a wedged API server can't hang before
  the bounded apply is reached: the nvidia/amd existence checks (bash + PS) and PS
  Confirm-GpuNode's node probe now carry --request-timeout=5s, matching bash's
  verify_gpu. Closes the "can't hang" goal symmetrically.
- Gate the amd master fallback on rollout like the primary paths: a master apply
  that never rolls out now warns + continues in CPU mode instead of returning a
  false success that made the caller's verify poll ~90s.
- Extract the shared _gpu_rollout_gate helper (nvidia, amd-primary, amd-master) so
  the "no false enabled / no dead verify wait" behaviour is identical everywhere and
  not duplicated three times.

Tests: adds _gpu_rollout_gate functional tests (rollout fail -> warn+non-zero; ok ->
success+0), asserts the existence probes are --request-timeout-bounded, that both amd
paths gate on rollout, and that the PS probes carry --request-timeout. Updates the
in-branch nvidia-gating test to the refactored (helper-delegated) form.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the install-k8s.ps1 + manifest.sha256 conflict from #588 (graceful
error boundary). Re-applies the two -Encoding UTF8 edits to the log writers on
top of develop and regenerates the R8 manifest hash.
…ead (#582) (#590)

* feat(installer): preflight network profile — proxy + TLS-inspection read (#582)

Child 1/4 of #578. On a restricted/corporate network the installer dove straight
into the long work and only failed minutes in, with no up-front sense of what the
network allows. Add a fast, bounded, non-fatal network probe that runs BEFORE the
endpoint checks and prints a plain-language "network profile" line:

- Explicit proxy detected from the environment (HTTPS preferred), announced as a
  bare host:port with any user:pass credentials stripped (PII-free, #576).
- A configured corporate CA bundle (TRACEBLOC_CA_BUNDLE / CURL_CA_BUNDLE) announced.
- TLS inspection detected affirmatively (best-effort): read the issuer of the cert
  served for a well-known public host (through the proxy when set); a non-public
  issuer means a corporate CA is re-signing TLS. Bounded and non-hanging — bash
  needs openssl + a timeout tool (else "unknown"); PowerShell uses .NET with an 8s
  timeout and a scoped, restored cert-validation callback.

A plain direct connection stays silent (the reachability lines already confirm
egress). The existing break-and-inspect hint keeps owning the actionable CA fix.
Cross-platform with one-to-one copy (preflight.sh _pf_network_* / install-k8s.ps1
Show-NetworkProfile). preflight.sh + install-k8s.ps1 are manifested; manifest
regenerated. Tests: 9 bats + 7 Pester covering credential stripping, proxy
precedence, issuer classification, the silent-direct path, and the noteworthy line.

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

* fix(installer): TLS-inspection probe must connect through an authenticated proxy (#582)

Bugbot (Medium): the network profile stripped proxy credentials for DISPLAY (correct,
PII-safe) but reused that stripped host:port for the probe's own CONNECTION. On an
authenticated corporate proxy the HEAD/CONNECT then 407s, issuer capture fails, and the
profile returns "unknown" — so inspection is never reported on the exact TLS-inspecting
networks the probe exists to detect.

Connect with the real credentials; keep display stripped:
- preflight.sh: add _pf_env_proxy_raw (verbatim env value). _pf_detect_tls_inspection
  passes -proxy host:port plus -proxy_user/-proxy_pass (openssl >= 3.0) when the proxy
  carries credentials. _pf_env_proxy / the profile line stay credential-stripped.
- install-k8s.ps1: add Get-EnvProxyRaw. Get-TlsInspectionState builds the WebProxy from
  the raw URL and sets WebProxy.Credentials (NetworkCredential) when UserInfo is present;
  Show-NetworkProfile still uses the stripped Get-EnvProxy for the printed line.

Credentials reach openssl / the WebProxy only — never printed or logged. Tests: bash +
Pester assert the raw helper preserves creds while display strips, that the openssl
CONNECT carries -proxy_user on an auth proxy (with the password absent from the result),
and that the probe uses the credentialed proxy while the display path does not.

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

* fix(installer): harden the bash TLS-inspection auth-proxy path (#582)

Four Bugbot follow-ups on the bash credential-passing added last commit:

- [High] Password no longer sits in openssl argv (visible via ps / /proc/*/cmdline):
  pass it via env: — openssl reads $_TB_PROXY_PASS, exported ONLY inside the probe's
  command-substitution subshell, so it never reaches argv or the parent shell.
- [Med] The issuer pipeline is guarded with `|| issuer=""`, so a failed/timed-out
  probe returns "unknown" instead of aborting _pf_detect_tls_inspection under
  `set -euo pipefail` (mirrors _pf_probe_url). The bats setup now stubs the probe by
  default (like _pf_probe_url) so connectivity tests never hit the real github.com;
  the real-probe tests source preflight.sh fresh in a subshell.
- [Med] Username-only proxies (http://user@host, no password) no longer reuse the
  username as the password — split on ':' only when present.
- [Med] Proxy credentials are URL-decoded (_pf_urldecode) before use, matching the
  PowerShell peer's Uri.UnescapeDataString, so %-encoded secrets authenticate the
  same on Linux and Windows.

Tests: url-decode parity, env:-not-argv (password absent from openssl argv),
username-only-not-reused, and the openssl-absent "unknown" guard — all sourcing the
real probe in a hermetic subshell.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Re-resolves the recurring install-k8s.ps1 + manifest conflict; re-applies the two
-Encoding UTF8 log-writer edits on develop's tip and regenerates the R8 manifest.
…591)

Set-Content -Encoding UTF8 prepends a BOM on PowerShell 5.1, so the log started
with EF BB BF. Use the file's own no-BOM writer ([System.IO.File]::WriteAllText
with UTF8Encoding($false)); the Add-Content append path was already BOM-free.
Regenerates the R8 manifest hash.
…s (Bugbot, #591)

Start-InstallLog now writes the curated install log as UTF-8 without a BOM, but
Edit-Redaction -- which -Diagnose runs over every collected file before zipping,
including the copied install-*.log -- still read via `Get-Content -Raw` with no
encoding. On Windows PowerShell 5.1 a bare read decodes a BOM-less file as ANSI,
so it mangled every non-ASCII host path/message back into mojibake in the bundle
operators send: the exact corruption this PR set out to fix.

Pin `-Encoding UTF8` on that read. It reads the BOM-less log correctly and still
reads the Out-File -Encoding utf8 outputs (which carry a BOM on 5.1) fine -- the
BOM is detected and stripped. Regenerated scripts/manifest.sha256 for the new
install-k8s.ps1 hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(installer): pin -Encoding UTF8 on install-log writes
… curl (#583) (#592)

* feat(installer): wire the corporate CA into cosign/helm/git, not just curl (#583)

Child 2/4 of #578 — the single biggest lever for TLS-inspecting corporate networks.
A break-and-inspect proxy re-signs HTTPS with a corporate root CA; tools that don't
trust that root fail x509. curl already honored CURL_CA_BUNDLE and the k3d NODES got
the CA at cluster-create (#424), but cosign, helm and git got nothing — the class
behind both field failures (the k3d-checksum and the cosign/sigstore x509 failures).

Extend the SAME resolved CA to every host tool that doesn't inherit the system store:
- Bootstrap (install.sh / install.ps1): export SSL_CERT_FILE from TRACEBLOC_CA_BUNDLE
  / CURL_CA_BUNDLE before cosign runs, so keyless verification's HTTPS calls trust the
  corporate CA (cosign's Go client reads SSL_CERT_FILE). Not manifested (trust root).
- Main installer: wire_ca_trust (bash) / Set-ToolTrust (PS) export SSL_CERT_FILE +
  GIT_SSL_CAINFO (+ CURL_CA_BUNDLE) from the resolved bundle, run BEFORE preflight's
  probes and any download, so helm, git and curl all trust it. Plain-language line:
  "Trusting your company's certificate for cosign, helm, git and downloads." No-op
  when unconfigured; fails fast on a set-but-unreadable bundle. When no CA is provided,
  tools fall back to the system store (which enterprise IT usually populates) — no user
  knowledge of CAs required in that common case.

Tests: bats (wire_ca_trust exports/no-op/hard-fail; bootstrap cosign sees SSL_CERT_FILE
via a recording mock) + Pester (Set-ToolTrust exports/no-op; bootstrap sets SSL_CERT_FILE).
cluster.sh / install-k8s.* are manifested; manifest regenerated.

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

* fix(installer): don't re-export CURL_CA_BUNDLE when wiring the corporate CA (#583)

Bugbot (Medium): _bootstrap_wire_ca / wire_ca_trust / Set-ToolTrust exported
CURL_CA_BUNDLE derived from the resolved bundle. But CURL_CA_BUNDLE is replace-not-
augment, and TRACEBLOC_CA_BUNDLE is typically a corp-root-ONLY PEM (its documented
k3d-node use), so re-exporting it could REPLACE curl's working trust with a bundle
missing the public roots — breaking the manifest/sig fetches that were succeeding via
the system store. The PowerShell bootstrap already set only SSL_CERT_FILE.

curl already honors the user's own CURL_CA_BUNDLE natively, so we never re-export it.
We only wire the tools that had NO corporate trust before: cosign/helm/Go
(SSL_CERT_FILE) and git (GIT_SSL_CAINFO). This also makes the bash and PS bootstraps
symmetric (both set only SSL_CERT_FILE).

Adds a regression test asserting wire_ca_trust leaves a pre-set CURL_CA_BUNDLE intact
while still exporting SSL_CERT_FILE for the other tools.

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

* fix(installer): SSL_CERT_FILE is Linux-only for Go; be honest on Win/mac + fail fast (#583)

Two Bugbot follow-ups:

- [High] SSL_CERT_FILE is inert for cosign/helm on Windows AND macOS. cosign/helm are
  Go; Go reads SSL_CERT_FILE only on Linux — on Windows it uses the certificate store
  and on macOS the Keychain, ignoring the env var. My comment wrongly claimed modern
  Go honors it on Windows. Corrected:
    * Linux: keep SSL_CERT_FILE (effective) + GIT_SSL_CAINFO; announce cosign/helm/git.
    * macOS (wire_ca_trust): still set the vars but announce only git + downloads, and
      hint that cosign/helm read the Keychain (add the CA there, or use the offline path).
    * Windows (Set-ToolTrust): set GIT_SSL_CAINFO (Git-for-Windows is OpenSSL-backed);
      do NOT set SSL_CERT_FILE (inert/misleading); hint cosign/helm read the cert store.
    * Windows bootstrap (install.ps1): drop the inert SSL_CERT_FILE set entirely.
  The robust cross-platform cosign fix for a PEM-only CA is the offline bundle (#584).

- [Med] A set-but-unreadable CA bundle now fails fast with a clear "can't be read"
  message in both bootstraps, instead of silently no-opping and surfacing later as a
  generic cosign authenticity error.

Tests updated for the platform-accurate behavior: Linux vs macOS announce, Windows
sets only GIT_SSL_CAINFO (not SSL_CERT_FILE) + store hint, and the bootstrap fail-fast
on a bad CA path (bash + PS).

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

* fix(installer): don't clobber a pre-set SSL_CERT_FILE / GIT_SSL_CAINFO either (#583)

Bugbot (Medium): wire_ca_trust / Set-ToolTrust set GIT_SSL_CAINFO unconditionally from
the resolved (corp-root-only) bundle. GIT_SSL_CAINFO is replace-not-augment (same
OpenSSL contract as CURL_CA_BUNDLE), so a fuller pre-set git CA bundle got overwritten
and host git HTTPS could x509-fail on non-intercepted endpoints. The same applies to
SSL_CERT_FILE.

Apply the consistent rule everywhere we wire trust: only set a trust var the user
hasn't already set — never override their existing bundle. Covers SSL_CERT_FILE (bash
bootstrap + wire_ca_trust) and GIT_SSL_CAINFO (wire_ca_trust + Set-ToolTrust); curl's
CURL_CA_BUNDLE was already left untouched. Adds regression tests (bash + Pester) that a
pre-set SSL_CERT_FILE / GIT_SSL_CAINFO survives while an unset one is still wired.

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

* fix(installer): don't over-claim download trust; check CA readability on Windows (#583)

Two Bugbot follow-ups:

- [Med] Set-ToolTrust printed "…and downloads" trust the corporate CA, but on Windows
  downloads use the certificate store (Invoke-WebRequest/Schannel) which this path never
  configures — only GIT_SSL_CAINFO. Green message, still-failing fetch. The announce now
  names only what's actually wired (git), and the store hint covers cosign, helm AND the
  installer's downloads. Same over-claim dropped on Linux/macOS: curl "downloads" trust
  the user's own CURL_CA_BUNDLE (which we deliberately don't touch), so Linux announces
  "cosign, helm and git" and macOS "git" only.
- [Med] The Windows bootstrap CA fail-fast only tested existence (Test-Path -PathType
  Leaf); a present-but-unreadable file slipped through to a generic cosign error. It now
  also opens the file (mirrors bash -r and Resolve-CaBundle) and fails fast with a clear
  "can't be read" message.

Tests updated: Linux/macOS announce wording (no "downloads"), and the Windows success
line names only git while the store hint covers downloads.

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

* test(#583): make the new CA-wiring assertions enforce

The 20 assertions this PR adds to cluster.bats and install-bootstrap.bats
were written multi-assertion without `|| return 1`, so under bats only the
last command decided each test -- a regression in the cosign/helm/git CA
wiring would have passed silently. Same hardening #527 applies suite-wide;
appending it here keeps this PR green under #527's bats-hygiene gate
whichever lands first. Both suites pass with enforcement on. The Pester
additions need nothing: Should throws, so every assertion already enforces.

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

* fix(#583): on macOS, wire nothing — Keychain guidance instead (Bugbot ×2)

Two Darwin holes, same root: exporting trust vars the platform ignores.

- SSL_CERT_FILE: Go reads the Keychain on macOS, so the export helped neither
  cosign nor helm — while OpenSSL-backed curl DOES honor it, replace-not-
  augment, so a corp-root-only bundle shrank download trust for zero gain.
  Dropped from wire_ca_trust and platform-gated in _bootstrap_wire_ca
  (readability fail-fast still runs everywhere).
- GIT_SSL_CAINFO: Apple's system git (SecureTransport) ignores it, and the
  clone that matters most — Homebrew's own bootstrap — runs system git. The
  "Trusting your company's certificate for git" claim was false on Darwin.

Darwin now exports neither var and prints one honest hint: add the CA to the
login Keychain so git, cosign and helm trust it. Same decision, same reason
as Windows (store-based trust; no inert claims). Tests: Darwin announce
updated, Darwin exports-nothing pinned at both layers, and the Linux
bootstrap test now stubs uname so it doesn't flip on a macOS dev machine.
Manifest regenerated.

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

* fix(#583): say only what was actually wired (Bugbot)

wire_ca_trust and Set-ToolTrust printed the green "Trusting your company's
certificate…" even when every only-if-unset guard skipped its export --
claiming wiring that did not happen, and masking a pre-set bundle that may
still lack the corporate CA. Both now track wired vs kept per variable: the
success names only what was actually exported, and anything kept gets an
explicit "make sure that bundle includes your company's CA" hint instead.
Pinned on both layers: both-kept claims nothing, partial pre-set claims only
the wired half (bats), skipped export claims nothing (Pester). Manifest
regenerated.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Lukas Wuttke <lukas@tracebloc.io>
…0) (#595)

Pin every third-party (non-tracebloc, non-actions) action ref to the
full 40-char commit SHA it currently resolves to, with a trailing
exact-version comment (D10, RFC-BACKEND-1405). Behaviour-preserving:
no version changes, only removal of silent tag mutation.

Part of tracebloc/backend#1490.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ce (backend#1526) (#596)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…able (#525)

* fix(installer): make the absent-key path in _extract_yaml_value reachable

`_extract_yaml_value` piped into `grep`. On an ABSENT key grep exits 1; under
`set -o pipefail` that rc propagates out of the pipeline and out of the
assignment, so under `set -e` the function aborts at the assignment — making
the very next line, `[[ -z "$line" ]] && return`, unreachable in exactly the
shape it exists to handle.

Latent, not live: all three call sites (lines 209, 651, 652) use the `$( )`
command-substitution form, which suspends errexit for the function body. But
the documented contract is "empty when the key is absent", so a bare call is
the natural next refactor — and it would abort the install mid-step.

Fix is the house idiom already used in assess.sh and common.sh
`_chart_version`: `|| line=""` on the assignment. Catching any non-zero also
keeps the path reachable if `head -1` ever SIGPIPEs grep (141), the sibling
shape fixed in #522. Contract written down above the function.

Verified (bash 3.2.57, GNU grep):
  - bare call, absent key, errexit live -> before: exit 1 (aborts, `return`
    never runs) · after: exit 0, empty output, execution continues
  - `v="$(_extract_yaml_value …)"`, absent key -> exit 0, "" (unchanged)
  - found-key, quoting, and unreadable-file paths unchanged

Adds a bats case pinning the BARE-statement call under `set -euo pipefail`.
Mutation-tested: it fails against the unfixed function, so it cannot rot into
a no-op. Regenerated scripts/manifest.sha256 (R8 gate).

Fixes #523

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

* ci: re-trigger — no workflow fired on the PR-open event (empty commit)

Actions dispatched nothing for this PR: 0 runs on the branch 10 minutes after
open, while a sibling PR opened 3 minutes later got all 7. Not a paths/types
filter (standard-checks + chart-version-guard have no paths filter and also
did not fire), not a draft, not an incident (status green), and PR head ==
remote head == local head. GitHub-side miss on the open event; `synchronize`
re-dispatches all six gating workflows.

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

* fix(installer): drop head from the pipeline — a duplicate key must keep its value (Bugbot, #525)

The first fix (`grep | head -1 || line=""`) traded one failure for another:
on a DUPLICATE key, head exits after the first line and SIGPIPEs grep (141);
under pipefail the `|| line=""` fallback then wiped the successfully captured
value, so detect_installed_client could miss a clientId and fail open toward
overwrite. Capture every match and take the first line in the shell
(`${line%%$'\n'*}`) — no downstream consumer, so grep's rc is 1 exactly when
there is no match, which is the one case the fallback exists for. Regression
test pins the duplicate-key bare-call shape; manifest regenerated.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* test(bats): make every assertion enforce — 1240 were advisory

Under Bats (verified 1.13.0) only the LAST command in a test body decides the
result, so a failing assertion anywhere earlier is silently ignored:

  @test "middle failure ignored" {
    [[ "abc" == *"zzz"* ]]      # FALSE
    [[ "abc" == *"abc"* ]]      # TRUE (last)
  }                             # -> ok

This suite is written multi-assertion throughout, so most assertions could not
fail their test. Appending `|| return 1` makes them enforce.

Scope is about twice what it first looked. It is not only `[[ ]]`: single-bracket
`[ ... ]` has identical semantics and there are MORE of them (609 vs 574), plus 61
negated bare commands. 1240 assertions across 15 files — setup-linux.bats 296,
cluster.bats 153, install-client-helm.bats 146, common.bats 118, preflight.bats
102, and the rest smaller.

Only whole-line assertions INSIDE an @test body are touched. Helpers and
setup/teardown are excluded (a bare `return` there means something different), as
are the 9 control-flow `if/while` conditions and 18 lines already chained with
&& / ||. All files still parse (bats --count), no control-flow line was modified,
and nothing was double-appended.

TRIAGE RESULT: zero new failures. All 1240 were already true — the suite was
accidentally correct, so there was no hidden-bug vs stale-assertion split to
report. No assertion was deleted or weakened to reach green.

That result only means something if the hardening has teeth, so it was proven
rather than assumed. cluster.bats's "_augment_no_proxy: empty host NO_PROXY"
asserts 7 substrings and only enforced the last. Deleting `localhost` from
TB_NO_PROXY_DEFAULTS — the entry that keeps a corporate proxy from intercepting
loopback — is a real regression, and:

  mutated source + ORIGINAL tests  -> ok      (invisible)
  mutated source + HARDENED tests  -> not ok  (caught)

Guard, so the pattern cannot come back: scripts/tests/bats-hygiene.bats plus a
shared scanner, scripts/tests/unenforced-assertions.awk (one implementation, used
by the guard and by its own self-tests). Three tests: the suite is clean; the
scanner flags an un-hardened assertion and spares a hardened one; and it ignores
control flow, chained lines, helpers and HEREDOC BODIES. That last exclusion is
not cosmetic — the first version flagged its own fixture, which would have made
any future test embedding example bats source a false positive.

The guard was mutation-tested against the real suite too: un-hardening one line in
cluster.bats makes it fail, naming the exact file:line.

Gates: bats scripts/tests/*.bats -> plan 693, ok 693, not ok 0 (complete TAP run,
plan line checked — a truncated read can look green while half the suite never
reports); shellcheck --severity=error over the CI file set -> rc=0; check-style
clean; check-drift no drift; gen-manifest.sh --check current (only tests changed,
and tests are not part of the hashed set).

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

* fix(bats-hygiene): scanner sees internal-OR and negated-bare assertions (Bugbot #527)

Two Bugbot findings on the hygiene guard this PR introduces. Both real: the
guard could report "suite is clean" while assertions stayed advisory — the exact
failure mode the PR exists to close.

Measured semantics first (bats 1.13.0, bash 3.2 system bash), because the old
header's "only the LAST command decides" was too broad. Bats does run bodies
under errexit; exactly two classes escape it:

  [[ ... ]]   bash 3.2 (macOS system bash) does not fire errexit for a failing
              conditional expression — a middle one is ignored
  ! cmd       POSIX: a status inverted with '!' is never propagated, so this
              escapes on EVERY bash, CI included
  grep -q ... a plain bare command DOES fail the test — correctly not reported

1) Scanner skips internal-OR assertions — REAL.
   `[[ a || b ]]` is ONE assertion whose ||/&& is internal; it exits non-zero on
   failure like any other and needs `|| return 1` too. The scanner skipped every
   line merely CONTAINING ||/&&, and only matched `[`/`[[` that closed on the
   same line, so it missed both single-line internal-OR and multi-line forms.
   Rewritten to build a logical line (trailing backslash, or a newline inside the
   brackets) and to locate the closer that matches the opener, so only a TOP-level
   chain earns the exemption: `[[ a ]] || fail` still skipped, `[[ a || b ]]`
   flagged, and `||` appearing only inside a quoted grep pattern no longer hides
   an assertion. Multi-line offenders are reported joined, at their first line.

   Six offenders it now catches (Bugbot named two; four are the same class):
   install-bootstrap.bats:144 and :154 — mid-body, so genuinely advisory —
   common.bats:179, install-client-helm.bats:887, preflight.bats:542,
   summary.bats:73. All six now end in `|| return 1`.

2) Guard omits negated bare commands — REAL.
   The PR hardened 61 `! cmd` assertions but the guard did not cover the class,
   so a later unhardened one would pass unnoticed — and this is the class that is
   advisory on every bash, not just 3.2. The scanner now flags standalone
   `! cmd ...`, while sparing `! cmd || return 1`, `if ! cmd`, bare `cmd`, and
   `run ! cmd`. Zero live offenders: the 61 are all hardened.

Failing-test-first evidence, both directions verified by flipping the change:
  - two new bats-hygiene tests (internal-OR incl. both continuation styles and a
    pattern-only `||`; negated bare commands) fail on the old scanner, pass on
    the new one, and assert the spared cases so the scanner cannot over-report
  - with the new scanner and the un-hardened files, the "suite is clean" test
    fails and names all six offenders
  - end to end on real code: blanking install.sh's "not an immutable release tag"
    message left install-bootstrap.bats's two path-traversal tests GREEN before
    the fix and fails both after — an R8 regression the suite had been ignoring

Also made the scanner portable (\b and `close` are not safe in every awk) and
corrected the guard's header to the measured semantics.

Local: bats scripts/tests/*.bats 695/695, shellcheck --severity=error clean,
bash -n clean, gen-manifest.sh --check up to date, check-style.sh clean.

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

* test(chart-guard): harden the 46 assertions #530 added in parallel

CI went red on the merge commit, not on this branch. Diagnosis first, because the
answer changes the fix:

  #530 ("chart-guard: cover every published chart") merged scripts/tests/
  chart-version-guard.bats into develop at 2026-07-31T15:59Z — 40 minutes AFTER
  this branch's last green Installer-tests run (30642417378, head 91a5fdd,
  15:19Z). The file was written before this convention existed, so all 46 of its
  standalone assertions are bare. GitHub tests refs/pull/527/merge, so the guard
  correctly reported them.

NOT caused by the scanner rewrite. The OLD scanner, exactly as shipped in
91a5fdd, flags the same 46 lines on that file — byte-identical output:

  awk -f <91a5fdd's scanner> chart-version-guard.bats | wc -l  -> 46
  awk -f <new scanner>       chart-version-guard.bats | wc -l  -> 46
  diff of the two                                             -> identical

They are all plain single-line `[ ... ]` / `[[ ... ]]`, none of the classes this
PR's rewrite added. So the branch head would have gone red on the same merge
commit with or without my commit — this is develop drift meeting a guard that
only just started existing, which is the guard doing its job on the first file
that arrived after it.

Fix: merge develop and append `|| return 1` to the 46. No assertion reworded,
deleted or weakened; the guard is not relaxed to accommodate the new file.

Local, post-merge: bats scripts/tests/*.bats 718/718, scanner reports 0,
shellcheck --severity=error clean (incl. the new chart-version-guard.sh),
gen-manifest.sh --check up to date, check-style.sh clean.

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

* fix(bats-hygiene): a quoted <<TAG or a herestring is not a heredoc (Bugbot #527)

REAL, and the most serious of the three findings — it made the guard lie.

The heredoc detector matched `<<TAG` anywhere on a line, including inside a quoted
string. Once tripped, it looked for a BARE terminator line that never comes, so the
scanner silently ignored every remaining line in that file while the "suite is
clean" test still reported clean.

Proven on the real file, not just in theory. Appending an unhardened assertion to
the end of bats-hygiene.bats — after its own `printf "  cat > f <<'EOF'\n"`:

  awk -f unenforced-assertions.awk bats-hygiene.bats   -> NO OUTPUT (invisible)
  bats bats-hygiene.bats                               -> ok 1 ... assertion ...
                                                          ends in '|| return 1'

The guard reporting clean while a bare assertion sits in the file it is scanning is
the worst failure this PR could ship, since every other claim in the PR rests on
that scan.

A SECOND live instance Bugbot did not name: `<<<` herestrings. The regex matched
from the second `<` of `run guard_leftover_data <<< "r"`, taking tag `r`, so
leftover-guard.bats was swallowed from line 131 onward — the same canary appended
there was equally invisible. Bugbot's Additional Locations listed only
bats-hygiene.bats#L135-138.

Fix, three parts:
  - `quoted_at()` walks shell quoting state, so a `<<TAG` inside '...' or "..." is
    text, not a redirection
  - `<<` immediately preceded by `<` is a herestring, not an opener
  - safety valve: an @test at column 0 ends heredoc-skip mode, so no future
    mis-detection can ever hide more than one test's worth of lines

Real heredocs still skip their bodies: 12 genuine openers across the suite are
still detected, and the pre-existing "ignores ... heredoc bodies" test fails if the
tracking is deleted rather than fixed — so "stop tracking heredocs" cannot pass as
a fix. Verified by disabling it: that test flips to not ok. Worth recording that
the suite-clean scan CANNOT catch that regression (no real heredoc body in the
suite contains a bare bracket line), so the fixture test is the only guard on it.

Two new tests, flipped in both directions:
  old scanner -> not ok 5 (expected line 3 to be flagged)
                 not ok 6 (expected line 6 to be flagged)
  new scanner -> ok 5, ok 6
Each fixture carries several distinguishable entries and asserts the exact offender
count plus the spared lines, so neither can pass by over-reporting or by a fixture
too small to tell an anchored rule from a loosened one.

Local: bats scripts/tests/*.bats 720/720, scanner reports 0, shellcheck
--severity=error clean, gen-manifest.sh --check up to date, check-style.sh clean.

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

* test(bats): enforce assertions in develop's newly-merged tests (#527 hygiene)

Merging develop brought in test files/tests added after this branch forked
(check-facts.bats, index-invariants.bats, setup-macos-lifecycle.bats, and new
preflight.bats cases) whose standalone assertions were written in the bare,
advisory form. bats-hygiene.bats — the enforcing-assertion guard this PR adds —
correctly flagged 134 of them. Append `|| return 1` to each so every assertion
can fail its test, exactly as this PR does across the rest of the suite.

Mechanical: `|| return 1` inserted before any trailing inline comment; negated
bare commands (`! grep …`) get the same enforcing form. Verified by re-running
the scanner to zero offenders and the full bats suite.

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

* test(bats): scanner enforces on real `|| return 1`, not the substring (Bugbot)

The enforcing check was a line-wide substring match for `|| return 1`, so an
assertion that merely MENTIONED the marker was treated as hardened though it does
not enforce: `[[ "$output" == *"|| return 1"* ]]` (marker inside a quoted pattern)
or `[[ "$x" == y ]]  # ... || return 1` (marker only in a trailing comment) slipped
through the guard (Cursor Bugbot, Medium).

Add `strip_comment` (drop an unquoted trailing comment) + `is_enforcing` (require a
`|| return 1` that is outside quotes and outside the comment), reusing the existing
quote walker. New bats-hygiene self-test proves both fooling shapes are flagged and
a real top-level `|| return 1` is still spared. Whole-suite sweep still reports 0
offenders, so the 134 conversions in the prior commit remain correctly recognized.

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

* test(bats): scanner catches compound-line assertions; harden the 107 it revealed (Bugbot)

The unenforced-assertions scanner classified a line as a bracket assertion only
when it OPENED with `[[`/`[`, so a mid-line assertion — `run x; [[ ... ]]`, the
last command of a compound line — was invisible. On bash 3.2 that `[[` still
cannot fail the test, so 107 such assertions across preflight/check-drift/setup-*
were advisory: the exact failure mode this PR closes.

- Scanner: check the last `;`-segment of a compound line for a standalone bracket
  assertion or negated bare command (`last_segment` + `classify`). Quote-aware;
  `bracket_tail` distinguishes an internal `||` from a real top-level chain.
- Harden the 107 revealed assertions (append `|| return 1`, before any trailing
  comment). No test logic changed — 304 suite tests still pass, 0 failures.
- bats-hygiene.bats: regression test for the compound-line case.

The other Bugbot findings on this PR (internal-OR, negated-bare, substring,
false-heredoc) were already handled by earlier commits; this closes the last one.

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

* test(bats): scanner robust to nested braces, one-line tests, subshell semicolons (Bugbot)

The hygiene scanner reported "clean" while missing real unhardened
assertions in three shapes Bugbot flagged:

  - a nested `name() { ... }` stub's column-0 `}` ended the @test scan
    early (check-drift.bats had an unhardened `[ "$_drift" -ge 1 ]`
    after a helm() mock) -> track brace DEPTH, not the first `}`.
  - one-line `@test "x" { run ...; [ ... ]; }` bodies were consumed as a
    bare opener and never scanned (common.bats had two) -> scan the
    inline body after the opening `{`.
  - an assertion that is not the LAST statement of a compound/one-line
    body -> classify each `;`-separated statement (subsumes the earlier
    last_segment hack, more correctly); paren-aware so a `;` inside a
    `( )`/`$( )` does not split a hardened `! ( a; b ) || return 1`, and
    comment-aware so a `;` inside a trailing comment is not split either.

Hardens the 26 assertions the improved scanner then surfaced:
cluster.bats / assess.bats and the new #542/#547 check-facts tests (all
pulled in by the develop merge), plus check-drift.bats and the two
common.bats one-liners. Adds 3 regression tests (nested braces, one-line
bodies, subshell `;`).

Full suite 804/804; hygiene 12/12; scanner clean.

* test(bats): top-level chain must ignore quotes/subshells; join mid-line multiline brackets (Bugbot)

Two more scanner gaps Bugbot flagged on the rewrite, both real:

  - the `||`/`&&` "already chained" exemption matched the operator
    anywhere in the statement, including inside a quoted pattern
    (`! grep -q "a||b" f`) or a `( )` subshell -> an unhardened negated
    command was silently treated as chained. Now a quote- and paren-aware
    top-level scan (`has_toplevel_chain`).
  - `bracket_open` only saw a continued `[[`/`[` at the START of the
    logical line, so a bracket opening mid-line (`run x; [[ a ||`
    continued onto the next line) was never joined -> a multi-line
    compound bracket stayed invisible. Now also checks the last
    `;`-segment.

Adds 2 regression tests. Full suite 806/806; hygiene 14/14; scanner clean.

* test(bats): scan one-line bodies whose bracket abuts the group closer (Bugbot)

A one-liner with no `;` before `}` (`{ … [ a ] }`, or the no-space `[ a ]}` /
`[[ a ]]}` where the closer is not recognised) left a `}` in the assertion's
post-closer tail, so it read as non-standalone and stayed invisible. Strip the
one-liner's group-closing `}` before classifying.

Valid bats needs a `;` before `}` (verified: `f() { [ 1 = 1 ] }` is a bash
syntax error), which already splits the assertion off — so this is defensive
for the degenerate shapes, not a live suite offender.

Regression test 15. Full suite 807/807; hygiene 15/15; scanner clean.

* test(bats): make the bracket-closer finder quote-aware (Bugbot)

after_close matched a blank-delimited `]]`/`]` by a word-boundary heuristic but
never walked quote state — the one structural walker that wasn't quote-aware. A
closer inside a quoted pattern (`[[ "$x" == "a ]] b" ]]`, `[ "$x" = "] y" ]`) was
mistaken for the real closer, so the assertion read as non-standalone and an
unhardened offender could slip through. Require the closer position to be unquoted
(reuses quoted_at), matching split_segments / has_toplevel_chain / brace_delta /
after_first_brace / strip_group_close.

Regression test 16. Full suite 808/808; hygiene 16/16; scanner clean.

* test(bats): harden the 24 assertions that arrived via the develop merge

The hygiene gate went red on its own merge commit: develop gained
gpu-nvidia.bats (2 advisory assertions) and the #582 network-profile block
in preflight.bats (22 more) after this branch's sweep. Same mechanical
treatment -- append `|| return 1`, comments preserved in place. Scanner
reports 0 offenders; gpu-nvidia, preflight and bats-hygiene suites pass.

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

* test(bats): a || return 1 inside a subshell is not hardening (Bugbot)

is_enforcing scanned for an unquoted `|| return 1` anywhere in the statement,
so `! ( cmd || return 1 )` was spared — but that return only exits the
subshell while the `!` still escapes errexit, leaving the statement advisory.
Rewritten on the same quote+paren walker as has_toplevel_chain: only a
top-level `|| return 1` counts. Fixture pins both subshell shapes (`( )` and
`$( )`) flagged and both top-level shapes spared; the full-suite scan stays
clean, so no real assertion was relying on the loophole.

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

* test(bats): a <<TAG in a comment is not a heredoc opener (Bugbot)

heredoc_tag_of was quote-aware but not comment-aware, so a trailing comment
DOCUMENTING heredocs opened skip mode with no terminator coming and the rest
of the @test body was silently swallowed — live in this very suite, where
bats-hygiene.bats comments mention <<TAG. Scan the comment-stripped line;
strip_comment returns a prefix, so positions stay aligned for the quote and
herestring look-arounds. Fixture pins: comment-mention doesn't skip, a real
heredoc still does, and scanning resumes after its terminator.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: shujaat hasan <shujaat@tracebloc.io>
…eqs hang (#593)

* fix(ci): bound the two unbounded network waits behind the ubuntu Prereqs hang

Three times on 2026-08-04 (#525, #592) the "Prereqs — ubuntu:*" matrix jobs
died at the 20-minute job timeout with nothing in the log but "Installing
Docker…", and once more failed in 20 seconds with a registry-1.docker.io
timeout (exit 125). Two unbounded waits, one per layer:

- Workflow: `docker run` pulls the distro image implicitly with no timeout,
  so Hub connectivity trouble either failed fast (exit 125) or stalled the
  whole job. Both container-matrix jobs (distro-prereqs, path-persist) now
  pre-pull with three bounded attempts (timeout 300 + backoff) and an honest
  "runner-to-registry connectivity, not this PR" error.
- setup-linux.sh: the get.docker.com convenience script's internal
  apt/download.docker.com fetches carry no timeout, so a stalled connection
  hung silently behind the spinner. The run is now bounded at 10 minutes
  (healthy installs take 1-3) and fails with a clear stalled-download message
  telling the operator to re-run; the fetch of the script itself already had
  retry + curl_secure timeouts. Same shape as the existing dpkg-lock and
  kubectl-fetch bounds.

New bats test pins the timeout bound on the get.docker.com branch (hardened
with || return 1 for the incoming #527 hygiene gate). Manifest regenerated.

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

* fix(ci): distinguish a stall from a real failure; keep the pull budget small (Bugbot ×2)

- setup-linux.sh: `if ! spin_cmd …; then error "stalled 10 minutes"` fired on
  ANY failure, mislabelling a fast real apt/script error as a stall — and it
  bypassed the existing spin_cmd_bounded helper, which returns 124 only on the
  deadline and tails the log on every failure. Switched to it: rc 124 gets the
  stalled-download message, any other rc gets an honest install-failed message
  pointing at the log tail. Harness gains a default spin_cmd_bounded mock; the
  bats test now pins the helper + its 600s bound.
- installer-tests.yaml: three timeout-300 attempts + backoff could eat ~16 of
  the job's 20 minutes, so a late-succeeding pull just moved the death from
  the pull to the install. Bounds resized (3 × timeout 90, 10/20s backoff,
  ~5.5 min worst case) so the job keeps most of its budget; a healthy pull
  takes seconds.

Manifest regenerated.

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

* fix(installer): prepare-host gets prepare-host re-run advice (Bugbot)

The new get.docker.com stall/failure errors always said "re-run the
installer" — but with TB_PREPARE_HOST_MODE set that points an admin at a
full provision as themselves, the exact outcome prepare-host exists to
prevent. Pick the re-run verb by mode, matching the daemon-check errors
later in the same function. Manifest regenerated.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… PVC mounts (RFC-0003 D9 Track B, client-runtime#203) (#594)

Companion to client-runtime#261: flag-gated ClusterRole grants for the PV
provisioning/GC path (persistentvolumes create/get/list/patch/delete,
persistentvolumeclaims create/list/delete) + PER_DATASET_PVCS=1 into
jobs-manager. Default off: byte-identical rendering. clusterScope: false
+ perDatasetPvcs fails the render (PVs are not namespace-grantable).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#589) (#600)

Bugbot on the staging mirror flagged 4 issues in the corporate-proxy / TLS-
inspection probe (from #590/#582):

- HIGH: the TLS probe cleared an already-captured issuer via `|| issuer=""`;
  openssl s_client often exits non-zero (SIGPIPE after x509) even on a good
  handshake, so MITM networks read as 'unknown'. Use `|| true` (empty capture is
  still caught below).
- proxy password url-decode ran the whole string through printf '%b', mangling
  literal backslashes; escape them first so only percent-escapes expand (PS parity).
- `openssl -help | grep -q` under pipefail dropped -proxy_user on authenticated
  proxies (grep -q closes the pipe, openssl gets SIGPIPE); capture then match.
- PS Get-TlsInspectionState: [System.Uri] rejected schemeless proxy.corp:8080;
  prepend a scheme like the display path already does.

Regenerated the R8 manifest for the install-k8s.ps1 change.
…Rekor (#584) (#599)

* feat(installer): offline Sigstore bundle — verify --offline, no live Rekor (#584)

Child 3/4 of #578. Keyless cosign verification needs sigstore's Rekor at verify time,
and our short-lived keyless cert is expired by install time — so a network that blocks
or TLS-inspects sigstore fails verification even for a valid signature (the class behind
the field Windows failure on a TLS-inspecting corporate network that #583's CA wiring
does NOT cover, since Go ignores SSL_CERT_FILE on Windows/macOS). Skipping the tlog was
proven not to verify the expired cert; the offline bundle carries the Rekor inclusion
proof (SET) so the cert's validity at signing can be established without any live call.

- Signing (release-helm-chart.yaml): cosign sign-blob now also emits
  manifest.sha256.bundle and publishes it as a release asset, alongside the .sig/.cert.
- Verify (install.sh + install.ps1): prefer `verify-blob --bundle <bundle> --offline`
  (full check — signature + cert identity + tlog inclusion — with NO live Rekor). Falls
  through to the existing online .sig/.cert keyless path for releases cut before the
  bundle existed, or if the bundle doesn't verify — the SAME full check, just needing
  live Rekor, so it's a fallback, never a downgrade.
- PS: extracted Invoke-CosignVerifyBlob so the fail-closed sentinel (nonzero
  $LASTEXITCODE seed) + stderr suppression (#576) back BOTH paths from one place.

Tests: bootstrap prefers --bundle --offline when a bundle is published and does NOT hit
the sig/cert path; falls back to sig/cert when no bundle (older release); fail-closed on
a bad signature unchanged. Pester + bats green.

SEQUENCING: needs a NEW release cut after merge so the bundle asset exists; until then
the fallback (online) path runs. Existing releases keep working via the fallback.

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

* test(#584): cover the bundle-fails -> sig/cert fallback + both-fail fail-closed (reviewer)

saadqbal: the bundle-present-but-verify-fails -> sig/cert fallback branch was never
exercised (both bundle tests forced cosign exit 0; the fail-closed test had no bundle),
so a regression there would stay green. Add:

- bats: a fallback case (bundle published, cosign REJECTS the --bundle verify but ACCEPTS
  sig/cert -> the sig/cert path runs and the install proceeds) and a both-fail case
  (bundle present, every cosign verify fails -> fail closed, privileged step never runs).
- Pester: behavioural (not source-text) equivalents driving Confirm-ManifestSignature —
  bundle-fails falls through to sig/cert (2 verify calls, no throw); both-fail throws the
  authenticity error.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…r/offline (#585) (#601)

* feat(installer): detect a blocked container registry + guide to mirror/offline (#585)

Child 4/4 of #578, first slice: detection + clean guidance (the acceptance's core;
the mirror/air-gap mechanisms + the offline bundle are follow-on slices).

Some sites hard-block Docker Hub / GHCR outright — the images aren't reachable
directly at all (distinct from a proxy or TLS-inspection). The preflight connectivity
check already probes the registry hosts; now, when the blocked hosts are specifically
the CONTAINER REGISTRIES, the installer surfaces the mirror / offline options in plain
language instead of leaving only the generic egress hint — and it stays a clean
preflight stop, never a raw pull failure (builds on #576/#577/#582).

- preflight.sh: after the connectivity hints, if any failed critical is a registry
  host (registry-1.docker.io / auth.docker.io / ghcr.io), print mirror/offline guidance
  pointing at docs/INSTALL.md.
- install-k8s.ps1: same, via a $regBlocked flag in Test-Preflight.
- docs/INSTALL.md: new "Blocked container registry (mirror / air-gapped)" section —
  point the install at a reachable mirror via TRACEBLOC_VALUES_FILE overriding
  images.*.registry (+ dockerRegistry creds), or an air-gapped bundle for fully offline
  sites, with the honest limit stated.

Tests: bats — the registry-block guidance fires when a registry is blocked and does NOT
fire when only a non-registry host fails; Pester — Test-Preflight carries the detection
+ guidance + docs pointer. preflight.sh + install-k8s.ps1 are manifested; manifest
regenerated.

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

* test(#585): enforce the new registry-guidance assertions (|| return 1)

The bats-hygiene gate (#527) flagged the four bare `[[ … ]]` assertions in the two
#585 preflight tests as advisory — under bats a non-final bare test can't fail its
@test. Append `|| return 1` so they actually enforce. Fixes the "Unit tests" +
"bats (bash unit, mocked)" CI failures on this branch.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ow) (#598)

* test(e2e): full seal suite vs the dev backend — backend#1184 deferred fast-follow

New e2e-full-seal.sh installs the working-tree chart on real k3d as the
dedicated dev e2e-test-agent (real credentials, CLIENT_ENV=dev), waits for
every release PVC to Bind and jobs-manager to hold a real backend session,
then runs helm test UNFILTERED — egress-enforcement + backend-reachability
+ storage-assertions in one release, hook-presence-guarded so a regated
check can't vanish silently. New helm-ci job full-seal-e2e runs it on
push/dispatch only and skips green with a notice until the e2e-test-agent
secrets (TB_E2E_CLIENT_ID / TB_E2E_CLIENT_PASSWORD) are provisioned.

The egress positive control moves verbatim into e2e-common.sh (one copy,
shared with e2e-seal-check.sh). SEAL-CHECK.md gains the CI-coverage map and
drops three follow-ups that shipped since (#541 live probe, cli#393
verdict, cli#449 matrix).

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

* fix(e2e): address Bugbot findings on the full-seal harness

- credentials travel in a mode-0600 temp values file, never on argv
  (process-list exposure on a shared runner + helm --set comma/brace
  mangling); removed on every exit path — installer parity
- the PVC wait does one guarded fetch per iteration, so a transient
  kubectl failure retries until the deadline instead of aborting under
  set -euo pipefail
- full-seal-e2e job timeout 30m -> 45m: the script stacks a 300s PVC wait,
  two 300s rollouts and a 600s unfiltered helm test on create_cluster's
  15m bound — GHA must not kill a slow-but-healthy run

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

* fix(e2e): local-path provisions RWO only — pin pvcAccessMode on the full-seal install (Bugbot)

The chart's PVC default is ReadWriteMany; rancher.io/local-path never
provisions it, so every claim sat Pending and both the Bound pre-wait and
storage-assertions were guaranteed to fail once the job activates. The
installer writes pvcAccessMode: ReadWriteOnce for this exact storage path
— the harness now installs with the same value.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rap messages (#581) (#602)

* refactor(installer): scrub remaining internal identifiers from bootstrap messages (#581)

Follow-up from the #576/#579 review (saadqbal). The cosign verification-failure message
was sanitized in #579; this sweeps the remaining lower-signal internal identifiers still
in user-facing bootstrap messages — the `manifest.sha256` release-asset filename and the
`RFC-0001 R8` internal spec code — to plain language, for consistency. No behaviour
change: fail-closed paths, verification logic, and exit codes are untouched; only the
wording of echo/throw/Warn strings changes.

- install.sh: "manifest.sha256" -> "the installer's integrity checksums" / "signed
  checksum list"; ".sig/.cert not published" / "authenticate the manifest" ->
  "the installer's signature isn't published" / "confirm the download is authentic".
- install.ps1: same wording, plus `(RFC-0001 R8)` removed from every throw
  (Resolve-InstallRef, Confirm-ManifestSignature, Confirm-ScriptIntegrity, the temp-dir
  guard). RFC-0001 stays in code COMMENTS (not user-facing) and the actual
  manifest.sha256 file paths/URLs are untouched.
- Tests: updated the two pinned assertions (install-bootstrap.bats + install.Tests.ps1)
  from "no entry in manifest" to "isn't in the installer's signed checksum list".

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

* fix(#581): scrub the PS AllowUnverified warn too (Bugbot)

The unverified-opt-in warn on the sig/cert path still said "manifest signature/cert
not published" while the bash warn and the PS throw on the same path were already
updated to "The installer's signature isn't published". My grep keyed on
"manifest.sha256", so this "manifest signature/cert" variant slipped through. Aligned
it with the rest. (The generic word "manifest" in the cosign-unavailable messages is
descriptive supply-chain English, not the filename/spec-code #581 scopes, so it stays.)

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

* refactor(installer): align cosign-unavailable messages to #576 vocabulary

Address saadqbal's consistency nit on #602: the two cosign-unavailable
strings still said "signed manifest" / "manifest signature" while the
sibling sig-not-published messages just below now say "the installer's
signature". Align both the WARN (AllowUnverified) and the fail-closed
throw/echo in install.ps1 and install.sh, and update the matching
install-bootstrap.bats assertion.

No behaviour change: only wording of the echo/throw/Warn strings.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…Registry (#604)

* feat(#585): re-home all images onto a private mirror via global.imageRegistry

Restricted-network / air-gapped installs can now point every image the chart
pulls at a private registry mirror with a single knob.

- Chart: global.imageRegistry (Bitnami convention) re-homes tracebloc/*, the
  spawned ingestor + training-job images (JOB_IMAGE_HOST), and the alpine/*,
  ubuntu/squid, busybox and curl helper images. Empty/unset renders
  byte-identical to before (docker.io / ghcr.io). An explicit
  images.ingestor.repository still wins; an explicit per-image registry is
  overridden by the global mirror. New tracebloc.mirrorPrefix helper for the
  registry-less utility images.
- Installer one knob: TRACEBLOC_IMAGE_REGISTRY writes global.imageRegistry into
  the generated values; TRACEBLOC_REGISTRY_USERNAME / TRACEBLOC_REGISTRY_PASSWORD
  also mint the imagePullSecret (server derived as https://<host>). Bash
  (_image_mirror_yaml) and PowerShell (Get-ImageMirrorYaml) parity.
- docs/INSTALL.md: rewrote the blocked-registry / air-gap section (the old
  per-image images.<name>.registry override was a no-op against this chart).
- Tests: helm-unittest suite (15), bats (7), Pester (7).

Contributes to #585.

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

* chore(chart): bump to 1.9.14 (global.imageRegistry is chart content)

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

* chore(installer): regenerate manifest.sha256 for #585 installer changes

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

* fix(#585): always emit dockerRegistry.server (default Docker Hub) for creds-only (Bugbot)

The chart schema requires dockerRegistry.server whenever create is true. When
registry credentials were set without a mirror or an explicit server, the
generated values omitted server and helm install failed with a schema error
instead of authenticating to Docker Hub. Default the server to
https://index.docker.io/v1/ in that case (bash + PowerShell), and always emit
it. Adds bats + Pester coverage; regenerates manifest.sha256.

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

* fix(#585): don't let an empty global.imageRegistry drop the squid per-image registry (Bugbot)

dig treats the chart-default empty global.imageRegistry as present, so the squid
image never fell back to egressProxy.image.registry — an explicit per-image
registry was silently dropped to docker.io on fresh installs and
reset-then-reuse upgrades. Resolve the mirror with a "" fallback and | default
through to the per-image registry (then docker.io); the global mirror still wins
when set. Adds a helm-unittest regression case.

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

---------

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

Environment selection is internal to tracebloc — a real install always
resolves to prod (default) and a user never picks dev/stg. The commented
`# CLIENT_ENV: prod` in the top-level env block advertised it as a knob,
inviting misconfiguration during onboarding, so drop it.

Environment selection stays fully functional via the (internal)
values.schema.json contract and the ci/*-values.yaml files; only the
user-facing template stops exposing it.

Bump chart 1.9.14 -> 1.9.15: values.yaml is packaged chart content, so the
edit only reaches installs via a new chart version (chart-version-guard).

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

fix(chart): hide internal CLIENT_ENV from user-facing values.yaml (#605)
@LukasWodka
LukasWodka requested a review from saadqbal as a code owner August 5, 2026 13:27
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

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

…tl/helm) (#607) (#608)

* fix(installer): survive a proxy/AV-truncated tool download (k3d/kubectl/helm) (#607)

On a filtered corporate network a proxy or antivirus can truncate or block a
GitHub-release binary mid-transfer. install-k8s.ps1 had a single transport
(Invoke-WebRequest) and a single after-the-fact signal (the checksum), so a
blocked k3d download dead-ended at the cryptic "System tool checksum
verification failed" -- the #578 field failure. The winget fallback was also
dead (k3d has no winget manifest, so Rancher.k3d always returned "No package
found").

PowerShell (install-k8s.ps1):
- Get-VerifiedDownload tries three transports in turn -- Invoke-WebRequest, then
  curl.exe, then BITS. A different HTTP stack commonly succeeds where one is
  blocked/truncated.
- Test-DownloadComplete validates each result BEFORE the checksum: present, at
  least a per-tool size floor, and the expected magic bytes (MZ for .exe, PK for
  the helm zip). A short/error-page/altered payload is caught as a TRANSFER
  failure (distinct from a checksum mismatch) and the next transport is tried.
- kubectl / k3d / helm all route through it.
- Removed the dead k3d winget branch (verified: k3d has no winget manifest).
- Every-transport-failure throws one specific, actionable message (allowlist the
  hosts / exclude the tools dir from AV) instead of the cryptic checksum error.

Bash parity (lib/common.sh, lib/setup-linux.sh):
- _assert_download_size gates kubectl/k3d/helm downloads on a size floor before
  the checksum, so a truncated/blocked transfer reports the real reason. The
  Linux path already uses curl; TB_MIN_DOWNLOAD_BYTES lets the bats fetch mocks
  (tiny fixtures) relax the floor.

Tests: Pester (Test-DownloadComplete, 7) + winget-removal guard; bats
(_assert_download_size, 4); fetch mocks updated via the env hook. Manifest
regenerated.

Contributes to #578. Closes #607.

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

* fix(#607): exempt the PowerShell curl.exe fallback from the bare-curl style guard

curl_secure() is a bash helper and cannot exist in PowerShell, so the resilient
download's curl.exe fallback is a deliberate, flag-matched fetch. Mark both lines
(the transport + the presence check) with '# style-guard: allow'. Regenerates
the manifest.

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

* fix(#607): TLS 1.2 floor on the curl.exe fallback + validation can't abort the fallback loop (Bugbot)

- curl.exe fallback now passes --tlsv1.2, matching curl_secure's TLS floor, so it
  cannot negotiate below TLS 1.2 on the proxy networks this targets.
- Get-VerifiedDownload wraps the Test-DownloadComplete call in try/catch: a
  post-download I/O error (e.g. AV locking/quarantining the just-written file) now
  records a problem and tries the next transport instead of aborting the whole
  download — the recovery path is the point of this change.
- Pester source-guards for both; manifest regenerated.

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

* fix(#607): clean up the tmp tree when the size guard fails (Bugbot)

_assert_download_size calls error (which exits); it now removes the caller's
mktemp -d tree first (passed as $4) so a truncated/blocked transfer no longer
leaves a partial tool payload under /tmp — matching the checksum-mismatch
branches in _fetch_kubectl / _fetch_k3d_release / _fetch_helm_release. Adds bats
coverage for both the cleanup-on-failure and leave-intact-on-success paths;
manifest regenerated.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@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 Aug 5, 2026
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@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 Aug 5, 2026
Comment thread scripts/lib/cluster.sh
tracebloc-release-train Bot and others added 3 commits August 5, 2026 15:32
Pin every actions/* ref to the full 40-char commit SHA it currently
resolves to, with a trailing exact-version comment (D10,
RFC-BACKEND-1405). Behaviour-preserving: no version changes, only
removal of silent tag mutation. tracebloc/* refs stay on @main by
design; third-party refs were pinned under backend#1490.

Part of tracebloc/backend#1491.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@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!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit da014f9. 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 Aug 6, 2026
@tracebloc-release-train
tracebloc-release-train Bot merged commit cfef895 into main Aug 6, 2026
78 of 82 checks passed
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.

4 participants