Skip to content

release-train: staging -> main - #515

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

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

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Jul 31, 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 fleet-facing promotion: Helm spawn semantics and chart defaults change customer edges; Windows/Linux installers alter install failure modes and privileges; CI gates now fail closed on secrets and unbumped chart edits.

Overview
Release-train promotion bundling chart 1.9.9, installer hardening, and CI/supply-chain gates.

The Helm chart adds per-environment ingestor floating tags (channelTags + shared tracebloc.clientEnv so staging/production aliases match digest pinning), an opt-in perExperimentDbCreds path (tb_credmgr secret, jobs-manager env, flag-gated Secret delete RBAC), and bumps 1.9.7 → 1.9.9. CI now requires a Chart.yaml version bump when templates/values change, checks multi-arch on every spawnable ingestor tag, reads gh-pages index.yaml via the GitHub API (not CDN), arms gitleaks with a baseline and soft-fail: false, and adds Dependabot security-only for Actions.

Windows install-k8s.ps1 grows into a six-step flow: self-elevation, reboot resume (RunOnce + state file), split preflight vs tool install, bounded tracked installs with heartbeats, WSL update with web-download fallback, daily-user Docker/WSL provisioning, and a hard stop when Docker’s VM is below the memory floor. Linux/bash gains sudo-wrapped install refusal, portable checksum verify, smarter NVIDIA toolkit reconfigure (skip bounce when already default), and honest image-pull progress copy.

Docs/PR template tweaks (WSL install path, expand-then-contract checklist) and RFC-CLIENT-0003 (one installer core) are included.

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

saqlainsyed007and others added 30 commits July 29, 2026 13:32
… (#963) (#457)
* chore(chart): close values-schema gaps + drop dead override/code/docs (#963)
Contract fixes for the client Helm chart (re-verified against develop at
chart v1.9.6; the #963 audit was taken at v1.8.4):
- values.schema.json: add the six live-but-unvalidated keys so bad values
fail `helm lint` instead of silently passing —
egressReachabilityCheck.enabled, ingestionAuthz.{allowed,serviceAccountName},
networkPolicy.training.enforcementProbeTimeoutSeconds, podTokenSigningSecret,
podTokenTtlSeconds. Types/defaults/constraints taken from values.yaml and
the templates that consume them. helm lint passes.
- ingestor subchart: remove the dead `image.repository` key — no template
ever rendered it (jobs-manager spawns from the parent chart's
images.ingestor.repository). Kept image.digest (live). README's air-gapped
override rows now point at the authoritative parent-chart path.
- README: drop the hardcoded chart version (said v1.3.5 while Chart.yaml is
1.9.6) and point to Chart.yaml / the releases page, so it can't drift again.
- Delete the unwired check_docker_arch_mac function + its bats test (no call
sites) and the orphaned docs/eks.md (referenced nowhere).
Part of tracebloc/backend#963.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(installer): regenerate manifest after common.sh trim + develop merge
The #963 chart-contract cleanup dropped 48 dead lines from
scripts/lib/common.sh, changing its sha; the installer manifest wasn't
regenerated, so the Static analysis gate (gen-manifest.sh --check) failed.
Merging develop also refreshed preflight.sh/install-k8s.ps1 hashes.
Regenerate scripts/manifest.sha256 to match the working tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Syed Saqlain <syedsaqlain@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…h-pages (#467)
* fix(release): package charts AS the tag version + pre-releases skip gh-pages
Incident 2026-07-29: the v1.9.7-rc.1 pre-release packaged the client chart
from Chart.yaml's plain 1.9.7 and pushed it into the public helm index as
a STABLE version -- customers running helm upgrade would have received
staging content (removed from the index by hand, tgz deleted). Two layers
now prevent it: (1) helm package --version/--app-version from the release
tag, so rc charts carry the -rc.N suffix helm's pre-release rules key on;
(2) pre-releases never run the gh-pages index steps at all -- FR consumes
the release assets (stamped installer / chart tgz), the index is a
customer surface reserved for finals.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: gh-pages gates key on verify's tag-derived prerelease, not the frozen event (Bugbot)
github.event.release.prerelease is an event-time snapshot: after verify
demotes a mis-marked release, it still reads false, so the demoted rc
would have entered the public index anyway. verify now outputs effective
prerelease-ness derived from the tag shape (the same strict rule the
demotion uses) and all three gh-pages steps gate on that output.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: post-publish index-invariant job (manual leak catch -> CI)
After every release run: the public index must contain only stable-shaped
versions, and a prerelease run must not have indexed its own version.
Fails loudly; would have caught the 1.9.7 leak within a minute of it
happening instead of during manual FR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#452)
* feat(install): rootless Docker core + Tier-1 routing (opt-in)
Add install_rootless_docker() and a Tier-1 early-branch in install_linux
so a modern-kernel host with no runtime and no root can install entirely
in user space (RFC 0001 Tier 1 — the RFC's primary path). Gated behind
opt-in TB_TIER1_ROOTLESS=1; with the flag unset a Tier-1 host falls
through to the legacy privileged flow unchanged (validated default).
- install_rootless_docker: uidmap-helper precondition (defers to
prepare-host #1178 when absent — never self-sudo), no-sudo install via
dockerd-rootless-setuptool.sh or get.docker.com/rootless, user-scoped
systemctl --user + loginctl enable-linger, DOCKER_HOST export with
XDG_RUNTIME_DIR fallback, single docker-info verify (no retry loop).
- Tier-1 branch mirrors the Tier-0 early-return. Tools still install via
sudo here (_set_tools_target keys no-sudo off Tier 0 only) — tightening
that for rootless Tier 1 is deferred to slice 3 (#1221).
- 6 bats cases; scripts/manifest.sha256 regenerated (R8).
Closestracebloc/backend#1219
Part of tracebloc/backend#1177 · Epic tracebloc/backend#1168
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address Bugbot review on rootless Docker core (#452)
- Prepend ~/bin to PATH after the rootless install so this run's docker
info verify + later k3d/docker calls resolve the CLI the
get.docker.com/rootless fallback installs there (High).
- Bound the rootless `docker info` verify with a new shared _bounded
helper (timeout/gtimeout, mirrors probe.sh) so a wedged user daemon
can't hang a headless install (Medium).
- Guard the user-systemd bring-up under set -e: `systemctl --user … ||
true` (the bounded verify is the real gate) and `loginctl
enable-linger … || warn` (optional; fails on polkit-locked hosts even
when the daemon is up) (Medium).
Adds 2 bats cases (~/bin on PATH; systemd/linger failure falls through
to the verify). Manifest regenerated (R8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: correct the uidmap remedy message (Bugbot #452)
The missing-uidmap error claimed prepare-host would install the uidmap
helpers, but run_prepare_host only sets up privileged Docker + the docker
group — it never installs uidmap. Point at the two honest remedies
instead: install the `uidmap` package directly (rootless then works), or
run prepare-host to set up Docker so the researcher installs at Tier 0
(no rootless needed). #1220 folds this into the shared subuid/subgid gate
and teaches prepare-host to install uidmap for real.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(install): TODO(#1221) — rootless daemon needs user-scoped proxy config
Bugbot on #452 flagged that install_rootless_docker never configures a
corporate proxy for the user-scoped dockerd (the #244 _configure_docker_proxy
is sudo/system-scoped and the Tier-1 early-return never reaches it), so k3d
pulls of rancher/k3s time out on proxy-only hosts. Deferred to #1221 (the
k3d-on-rootless-socket slice that owns the pulls); leaving a tracked TODO so
the follow-up adds the user-scoped drop-in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: address Asad review nits on #452 — chmod no-op + misleading comment
- Drop the chmod +x on the rootless installer script: it runs via
`sh "$rootless_script"`, which ignores the exec bit.
- Reword the Tier-1 _install_userspace_tools comment: tools still
sudo-install on Tier 1 (only _persist_tools_on_path is no-sudo);
the comment previously implied otherwise.
The underlying _set_tools_target sudo-crash on no-sudo hosts and the
post-install DOCKER_HOST shell persistence are tracked to #1221.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: resolve user via id -un in install_rootless_docker (Saqlain review, #452)
$USER can be empty in headless / su / cron contexts (a Tier-1 target),
which would break `loginctl enable-linger` and the success line. Resolve
the user once via `id -un` (fallback $USER) and use it for the linger
call, its hint, and the success message. Matches the id-based robustness
DOCKER_HOST already uses. Happy-path bats now mocks `id -un` cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add pre-commit config (Layer 0, lint-only)
Lint-only on purpose: scripts/manifest.sha256 must keep matching the bytes
under scripts/, so no hook may rewrite files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Document pre-commit setup in README
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Part of tracebloc/backend#1308
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* ci: lint scripts/resolve-ingestor-digest.sh in CI shellcheck (was never linted)
Both CI shellcheck invocations enumerate files explicitly and both omitted
this script. Verified clean against shellcheck --severity=error --shell=bash
0.11.0 before adding. The pre-commit hook from #465 already covers it
locally; this closes the same gap on the CI side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci: lint scripts/resolve-ingestor-digest.sh in CI shellcheck (was never linted)
Both CI shellcheck invocations enumerate files explicitly and both omitted
this script. Verified clean against shellcheck --severity=error --shell=bash
0.11.0 before adding. The pre-commit hook from #465 already covers it
locally; this closes the same gap on the CI side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ilures (#468) (#469)
The v1.9.7-rc.1 FR killed a healthy install: PS 5.1's progress overlay
throttled the 17 MB pinned-cosign fetch to ~4.5 min of dead silence and
the window read as frozen.
- silence the PS 5.1 progress overlay in Get-WithRetry/Get-Optional
(function-local, auto-reverts) - the classic 10-50x IWR speedup
- run the cosign fetch in a background job with a dim liveness tick
(Wait-JobWithTicks / Get-OptionalWithTicks; cwd pinned per #409,
TLS 1.2 re-applied in the fresh process), expectation lines before,
elapsed + checksum-verified confirmation after
- ASCII-only string literals in both installers: the release asset is
served without a charset so PS 5.1's irm decodes UTF-8 source as
Latin-1 before iex, and BOM-less -File reads are ANSI - literal
em-dashes/ellipses reached customers as mojibake. Locked in by a
tokenizer-based Pester test (which also caught the -Help here-string).
- Enable-OneVirtFeature: translate DISM's raw COMException (feature
package absent on Server SKUs vs enable failure) and stop demanding
a reboot for a feature that never enabled (old code sent Server
users into a reboot->re-run->same-error loop)
Pester: 212 passed / 0 failed locally (pwsh 7.5, Pester 5.7.1).
PSScriptAnalyzer: 0 errors. manifest.sha256 regenerated.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…453)
* fix(installer): trust the corporate MITM CA in the k3d nodes (#424)
Proxy REACHABILITY reaches the nodes, but on a TLS-inspecting (break-and-
inspect) network the nodes still don't TRUST the corporate CA, so every in-node
containerd pull (rancher/k3s, ghcr.io, tracebloc images) fails x509 — then
masked (helm runs without --wait) into a root-cause-free "an image couldn't be
pulled." Enterprise/hospital archetype, all three OSes.
- Inject the CA at create time: when TRACEBLOC_CA_BUNDLE (or CURL_CA_BUNDLE) is
set, mount the bundle into every k3d node and write a registries.yaml pointing
containerd at it per-registry (docker.io, registry-1.docker.io, ghcr.io), via
the same --config/create path that already carries proxy env. Parity across
scripts/lib/cluster.sh (Linux/macOS) and install-k8s.ps1 (Windows). A CA var
set but unreadable fails loudly instead of silently skipping.
- Name the env var where the user hits the wall: the TLS-interception preflight
hint (both OSes), docs/INSTALL.md, and the PS -Help env-var list.
- CA-aware diagnosis: detect x509 / "certificate signed by unknown authority"
pull events and report a dedicated image_pull_ca state — "the cluster does not
trust your network's TLS-inspection CA" + the exact remedy — instead of the
generic pull error. Mirrored in summary.sh and Print-Summary.
- New check-drift.sh parity check (_drift_ca_trust) so neither installer can drop
the CA wiring for the other's OS.
Tests: +8 cluster.bats, +3 summary.bats, +2 check-drift.bats, +8 Pester.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): CA-trust hardening — no fail-open, bounded events, verify CA readable (Bugbot #424)
Three Bugbot findings on #424:
- _write_k3d_registries_config failed open: on mktemp failure it returned success
with no path, so create still mounted the CA and logged "nodes trust it" but
dropped --registry-config → containerd never got ca_file, x509 pulls still fail
while the operator thinks it's fixed. Now returns non-zero; the caller hard-errors
(CA was supplied, so we refuse to proceed without wiring it in).
- PS Get-NotReadyState `kubectl get events` had no --request-timeout (the bash path
does) — on a wedged/proxy-misrouted API, classification could hang. Added
--request-timeout=5s to match _diagnose_not_ready.
- PS Resolve-CaBundle only checked existence (Test-Path), not readability, so an
unreadable CA passed on Windows but bash (-r) hard-fails. Added an OpenRead probe
so both fail the same way, up front.
Tests: cluster.bats +mktemp-failure + unwritable-registries-hard-error;
install-k8s.Tests.ps1 +unreadable-CA (Unix) + events --request-timeout assertion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): errexit-safe CA-resolve capture + drift check ignores comments (Bugbot #424 r2)
Two round-2 Bugbot findings:
- Under `set -euo pipefail`, `ca_bundle="$(_resolve_ca_bundle)"; ca_rc=$?` exited on
the rc-2 (unreadable/missing CA) BEFORE ca_rc/error ran — operators got a bare
exit instead of the "can't be read" guidance. Capture with `|| ca_rc=$?` so
errexit doesn't fire and the guidance prints.
- _drift_ca_trust whole-file grep matched tokens in comments (e.g. --registry-config
appears in a comment above the real line), so deleting the functional wiring could
still pass. Strip comment lines first (matches the execute-gate / preflight-host
checks), no grep -q under pipefail.
Tests: cluster.bats +errexit-safe-capture; check-drift.bats +comment-only-token drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): TLS-preflight hint names the right var per layer/OS (Bugbot #424 r3)
The hint claimed TRACEBLOC_CA_BUNDLE makes "the host AND the k3d nodes" trust the
CA, but the host connectivity checks use curl_secure / Invoke-WebRequest, which
read CURL_CA_BUNDLE / the system trust store — not TRACEBLOC_CA_BUNDLE (that var
only reaches the nodes via _resolve_ca_bundle). Following the hint literally left
host preflight TLS failures unchanged. Corrected, no behaviour change:
- bash: CURL_CA_BUNDLE fixes these host checks AND the nodes; TRACEBLOC_CA_BUNDLE
is nodes-only; or add the CA to the system trust store.
- Windows: import the CA into the cert store for the host checks (Invoke-WebRequest
uses the store, not an env var); TRACEBLOC_CA_BUNDLE/CURL_CA_BUNDLE cover the nodes.
(Reworded to avoid a bare lowercase `curl` that the curl_secure style guard flags.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): apply CA on cluster REUSE path — warn + recreate guidance (Bugbot #424 r4)
The image_pull_ca remedy said "set the CA and re-run", but CA trust is baked in
only at fresh create; a re-run reuses the existing cluster and never mounts the CA
or passes --registry-config, so the x509 pulls persisted. Mirror the existing
proxy handling (baked-at-create → warn on reuse):
- bash _check_existing_cluster_ca (called from _handle_existing_cluster): warns when
a CA bundle is set but the reused server container lacks the CA mount.
- ps1 New-K3dCluster reuse block: same check via docker inspect mounts.
- both image_pull_ca remedies now say to `k3d cluster delete <name>` first, then
re-run with the CA (CA, like proxy, can't be added to a running cluster).
Tests: cluster.bats +3 (no-CA no-op / CA-but-missing-mount warns / mount-present silent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): add auth.docker.io to the CA registries config (Bugbot #424 r5)
The registries.yaml ca_file entries covered docker.io / registry-1.docker.io /
ghcr.io, but Docker Hub pulls also TLS-handshake with auth.docker.io for bearer
tokens — so on a break-and-inspect network containerd still rejected the
intercepted cert there even with the CA mounted. #416 already probes auth.docker.io
at preflight; the CA registries list now matches. Added to TB_CA_REGISTRIES and
$TbCaRegistries; registries.yaml test counts 3 -> 4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#424): _resolve_ca_bundle rejects a directory, not just unreadable paths
A directory of PEMs is readable (-r) but would bind-mount over the single
node ca_file path and containerd can't read it — the silent 'looks applied
but still x509' case. Require a regular file (-f), mirroring the PS
Resolve-CaBundle -PathType Leaf check. Adds a directory-reject bats case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#424): exact whole-line match for CA mount detection (Bugbot)
_check_existing_cluster_ca used a substring test on docker mount
destinations, so a longer path embedding /etc/ssl/certs/tracebloc-mitm-ca.crt
(e.g. …crt.bak) would be treated as the CA mount and skip the recreate
warning while containerd still x509-fails. Switch to grep -qxF (exact
whole-line), matching the PS anchored regex. Adds a substring-embed test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#424): scope x509 classification to the pull-failure event (Asad)
_diagnose_not_ready / Get-NotReadyState flagged image_pull_ca on ANY x509
event in the namespace, so a stale/unrelated x509 event (e.g. a FailedMount)
could misdirect the user into a needless delete+recreate. Filter events to
the image-pull failure lines (failed to pull / ErrImagePull) before testing
x509, in both bash and PS. Adds an unrelated-x509 test to each side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…nd#1205) (#472)
* feat(chart): perIngestionTables — the RFC-0003 D16 enablement knob (backend#1204/#1205)
values.perIngestionTables (default false, schema-typed) renders
PER_INGESTION_TABLES=1 onto the jobs-manager, which forwards it into
every ingestion Job it spawns (client-runtime companion PR). Flip per
environment, dev first, only once that environment's backend + engine
images + jobs-manager carry the merged D-series. Default installs
render byte-identically (conditional block; unit tests pin both sides).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(values): own banner for perIngestionTables — it is not part of the authz section (review)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fix every finding the shared org checker (tracebloc/.github
scripts/house-rules.sh) reports at develop HEAD: missing curl
timeouts/TLS floors, plus (cli) a missing pipefail. Waivers only where
the finding is a documented false positive. Part of tracebloc/backend#1303.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#1220) (#458)
* feat(install): Tier-1 subuid/subgid gate + prepare-host remediation
RFC 0001 #1220. Detect the one privileged residue a modern rootless host may
still need — a subordinate UID/GID range + the setuid uidmap helpers — and
either proceed (present), hand off to prepare-host (unprivileged), or perform
one announced touch (sudo available). Never blanket sudo, never an opaque
mid-install crash inside dockerd-rootless-setuptool.sh.
- probe.sh: _probe_subid_ranges (PROBE_SUBID) + _probe_uidmap_helpers
(PROBE_UIDMAP), set in run_host_probes (Linux only), plus audit rows on
the Tier-1 path.
- common.sh: shared pure parsers _subid_has_entry + _next_subid_start, used
by both the probe and the remediation (no duplication).
- setup-linux.sh: _ensure_subid_ranges gate (present / hand-off / one
announced sudo touch) called before install_rootless_docker;
_provision_subid_ranges (idempotent, non-overlapping block, usermod
--add-subuids with file-append fallback, uidmap install) shared by the
installer and run_prepare_host. Folds in slice-1's minimal uidmap check.
- Tests: probe.bats + setup-linux.bats. Manifest regenerated (R8).
Closestracebloc/backend#1220
Part of tracebloc/backend#1177 · Epic tracebloc/backend#1168
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): address #458 review — subid gate/probe/hand-off robustness
Bugbot + @saadqbal + a self code-review on client#458, all in slice-2's code:
- id -un everywhere (gate, _provision default, probe): $USER diverges from the
rootless daemon's user under su/cron, which wedged detection/provisioning (#1).
- Re-verify the uidmap helpers are usable (present AND setuid|cap_setuid) after
install, and return non-zero + warn (NOT error/exit) so run_prepare_host stays
best-effort while the installer sudo-path hard-fails via `|| error` (#2 + self-review).
- _idmap_helper_ok (common.sh): accept the setuid bit OR a cap_setuid filecap, so
Arch's `shadow`/pacman path isn't false-rejected (#3).
- Hand-off + run_prepare_host fallback compute a non-overlapping start via
_next_subid_start (honoring TB_SUBUID_FILE/TB_SUBGID_FILE), not hardcoded 100000
(#4 + self-review path-override).
- Hand-off command names the researcher (TB_PREPARE_USER=) — bare prepare-host
provisions nothing, so it would have looped back to the same hand-off (#5).
- Capture `usermod --help` before grepping — pipefail-safe (#6).
bats: id -un mocks, filecaps accept/reject, gate hand-off (names user + computed
start), _provision re-verify best-effort, run_prepare_host best-effort. R8 regen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(install): stub the subid gate in the Tier-1 rootless routing test
install_linux's Tier-1 branch now calls _ensure_subid_ranges (slice 2)
before install_rootless_docker; the routing test left it un-stubbed, so the
real gate hit the no-sudo hand-off and error()'d → install_linux returned
non-zero. Stub _ensure_subid_ranges (its own behavior is covered by the
dedicated gate tests) and assert it runs before daemon setup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(install): scope set -o pipefail to a subshell (bats harness footgun)
Setting `set -o pipefail` in the @test body can leak into bats' own
post-test pipelines and fail the whole run with exit 1 even when every
test reports ok (no 'not ok'). Confine it to a subshell around the call
so the pipefail-safety assertion still holds without touching the harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): style guard — no bare curl in the prepare-host hint
The hand-off piped 'curl … | TB_PREPARE_USER=… bash', which breaks
check-style.sh's exemption for the canonical 'curl … | bash' one-liner
(the env var sits between the pipe and bash). Split into an 'export
TB_PREPARE_USER=…' line + the canonical piped one-liner — still names the
researcher, and passes the guard. Verified with scripts/check-style.sh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): fix the #458 red bats + 2 Bugbot bugs (newgidmap cap, write-failure)
Root cause of the "540 ok but exit 1" bats red: the probe.bats uidmap tests set
PATH="$bin" in the test body to hide system helpers, which also hides `rm` — so
bats-core 1.10+ can't run its own per-test cleanup ("rm: command not found") and
fails the whole run even though every test passes. Scope the hermetic PATH to a
subshell so it can't leak into bats' machinery. (Why develop was green + this was
so hard to see: these tests are new in slice 2, and the symptom is a clean pass
list with a non-zero exit.)
Two real Bugbot findings in the slice's own code:
- _idmap_helper_ok checked cap_setuid for BOTH helpers; newgidmap carries
cap_setgid (Arch filecaps) -> false-rejected. Map name->cap; fix the test mock
that masked it + add a wrong-cap regression test.
- _provision_subid_ranges printed success/returned 0 even when the usermod/tee
write failed (callers run it with set -e off) -> installer proceeds with no
range. Guard every write; warn + return 1 on failure. + a test.
Verified: probe.bats + setup-linux.bats EXIT 0 (0 not-ok, 0 rm-not-found) in a
faithful ubuntu 24.04 + bats 1.10 + non-root container. Rebased onto develop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): 2 more Bugbot findings on #458 (apt hang, false zero-root message)
- _install_uidmap_pkg ran a bare `sudo apt-get install -y uidmap` under the
spinner — no needrestart/DEBIAN_FRONTEND env, no DPkg::Lock::Timeout, no
apt_wait_for_lock — so a headless Tier-1 install can hang on Ubuntu needrestart
or an apt-daily lock (#210 class). Reuse the repo's hardened PM_INSTALL
(populate via setup_pm, which Tier 1 skips) + apt_wait_for_lock.
- install_rootless_docker always printed "no administrator rights were used",
even after _ensure_subid_ranges performed an announced sudo touch on the
root/sudo_nopw path. The gate now sets TB_ROOTLESS_ADMIN_TOUCH and the summary
is honest on both the zero-root and one-admin-touch paths.
Tests: hardened-install assertion (NEEDRESTART_MODE + DPkg::Lock::Timeout) + a
success-message honesty test. Verified EXIT 0 (0 not-ok, 0 rm-errors) in the
faithful ubuntu 24.04 + bats 1.10 + non-root container.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): refresh the package index in _install_uidmap_pkg (Bugbot #458)
Completing the prior apt-hardening: _install_uidmap_pkg populated PM_INSTALL and
waited for the dpkg lock but never ran PM_UPDATE. On the Tier-1 path this is the
first package op, so an empty/stale index can't locate uidmap/shadow and the
install hard-stops. Run $PM_UPDATE (best-effort) first, matching the repo's other
install paths (setup-linux.sh:335/543). Test asserts the index refresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s1 downloads (#468 follow-up) (#471)
Same class as the bootstrap fix in #469: PS 5.1's progress overlay
throttles Invoke-WebRequest 10-50x and reads like a hang. One
function-local $ProgressPreference in Invoke-WithRetry covers every
fetch scriptblock it drives (dynamic scoping) - winget msixbundle,
Docker Desktop fallback, kubectl, k3d, helm, GPU plugin yaml, and the
version resolvers.
Honest-progress expectation lines (sizes measured today via HEAD):
Docker Desktop ~600 MB, winget ~200 MB, kubectl ~60 MB, k3d ~25 MB,
helm ~20 MB - all cold-path only, silent on warm re-runs.
Pester: 205 passed / 0 failed locally. PSSA: 0 errors.
manifest.sha256 regenerated.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: shujaat hasan <shujaat@tracebloc.io>
…untime image pulls (#474) (#475)
* fix(#474): detect host Docker daemon x509 at cluster-create + document daemon CA trust
k3d pulls its own runtime images (rancher/k3s, k3d-tools, k3d-proxy) with the
HOST Docker daemon, which doesn't use the in-node CA trust from #424. On a
TLS-inspecting network that pull can x509-fail during 'k3d cluster create',
before any node boots — so the post-create diagnosis never classifies it.
- bash: _host_ca_create_hint() detects x509 in the k3d create output and prints
a platform-aware remedy (Linux system trust store vs Docker Desktop VM);
wired into _create_new_cluster's failure path.
- PS: Write-HostCaCreateHint() mirrors it (Windows Trusted Root store), wired
before the generic create failure.
- docs/INSTALL.md: document trusting the CA in the daemon itself (Linux /
Docker Desktop).
- check-drift.sh: enforce both installers keep the host-CA hint (parity).
- Tests: bats (Linux/macOS branches + silent-on-no-x509) + Pester + drift.
Closes#474
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#474): distro-aware Linux CA remedy + actionable Docker Desktop for Linux (Bugbot)
- Linux native-Docker remedy now covers both Debian/Ubuntu (update-ca-certificates)
and RHEL/Fedora (update-ca-trust), not just the Debian path — the installer
supports RHEL hosts where the Debian commands fail.
- Docker Desktop for Linux now has an actionable step (trust in the system store,
restart Docker Desktop) instead of a dangling reference to a step only printed
on the macOS branch.
- docs/INSTALL.md updated to match. bats Linux test asserts both distro paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#474): cover Colima runtime in the macOS host-CA remedy (Bugbot)
Headless macOS installs use Colima (_install_docker_colima), a Lima VM that
does not read the macOS keychain — so the 'trust it in the keychain + restart
Docker Desktop' remedy was wrong for those hosts. The macOS branch now also
gives the Colima path (add the CA inside the VM via 'colima ssh', then
'colima restart'). docs/INSTALL.md + macOS bats test updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#474): isolate drift negative fixtures so a missing new token can't mask them (Bugbot)
Adding _host_ca_create_hint / Write-HostCaCreateHint as required _drift_ca_trust
tokens meant the older negative fixtures (missing registry-config, comment-only
registry-config) could pass just because the new token was also absent — so the
comment-strip case no longer uniquely proved comment-stripping still works. Each
negative fixture now carries ALL other required tokens and omits/comments only
the one under test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#474): surface host-CA hint on the PS create-timeout path too (Bugbot parity)
The PowerShell create-timeout branch exited via Err without calling
Write-HostCaCreateHint (and deleted the k3d logs first), so a TLS-inspected
host pull that logs x509 then hangs to the deadline gave Windows operators a
raw timeout with no certlm.msc CA guidance — while bash runs _host_ca_create_hint
on its timeout fall-through. Capture the full create output before deleting the
logs and call the hint before the timeout Err. Adds a parity regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#474): use a herestring in _host_ca_create_hint to survive pipefail (Asad)
printf '%s' "$out" | grep -qiE ... could swallow the hint under set -o pipefail:
grep -q closes the pipe on first match, so for output past the ~64KB pipe buffer
(reachable on the timeout path, which passes the full logs) printf takes SIGPIPE,
the pipeline exits non-zero, and `|| return 0` bails even though x509 matched.
Feed grep via a herestring (no pipe, no SIGPIPE). Adds a >64KB-under-pipefail
regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… (#1221) (#478)
* feat(install): Tier-1 k3d-on-rootless + cgroup delegation + autostart (#1221)
Slice #1221 (RFC 0001 / #1177): make a rootless Tier-1 cluster actually usable,
all behind the opt-in TB_TIER1_ROOTLESS flag (default off until the spike's §5
host validation). With the flag unset every path below is a no-op and current
behavior is byte-for-byte unchanged.
- Shared _rootless_active predicate (common.sh) so cluster.sh + setup-linux.sh
can't drift on the flag pair.
- create_cluster targets the rootless socket (DOCKER_HOST); ensure_cluster_
autostart gets a user-scope branch (systemctl --user enable + loginctl
enable-linger, never `sudo systemctl enable docker`), and promises
reboot-survival only when BOTH succeed (honesty rule, #375/#458).
- cgroup v2 controller delegation drop-in (Delegate=cpu cpuset io memory pids):
privileged write + daemon-reload on root/sudo, or hand off to prepare-host
with the exact path+content when unprivileged. run_prepare_host writes it too
(system-wide -> covers the researcher).
- Carry-ins from #452/#458: scope-aware _configure_docker_proxy (user scope, no
sudo) so a proxy-only host's rootless daemon can pull rancher/k3s;
_set_tools_target installs user-space on rootless Tier 1 (no sudo-mv crash on
a true no-sudo host); persist DOCKER_HOST to the shell rc for new terminals.
14 new bats tests incl. flag-off regressions; shellcheck --severity=error clean;
manifest.sha256 regenerated (R8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): address Bugbot on #478 — rootless autostart seed + admin-touch msg
- ensure_cluster_autostart: don't seed TB_DOCKER_AUTOSTART from the SYSTEM
docker.service is-enabled check on the rootless path. The cluster runs on the
per-user rootless socket, so a system unit that happens to be enabled would seed
a false reboot promise the rootless branch then can't honestly retract. On
rootless the user-scope enable+linger are now the sole authority (Bugbot medium).
- install_rootless_docker: the TB_ROOTLESS_ADMIN_TOUCH success line no longer
hardcodes "subuid/subgid range" — _ensure_cgroup_delegation can set that flag
too, so it now names "host prerequisites (subuid/subgid range and/or cgroup
delegation)" (Bugbot low).
- Test: rootless + system docker.service enabled + user-enable fails => flag stays
0 (pins the seed-guard). manifest regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): address Asad + Saqlain review on #478
- _persist_docker_host: key idempotency off our own marker, not a bare
'DOCKER_HOST=' probe. The old probe also matched a user's own DOCKER_HOST
(remote/TCP), so we silently skipped persisting the rootless socket and new
shells kept hitting the wrong daemon. Now: our own line -> idempotent; a foreign
DOCKER_HOST -> left untouched + a warn to repoint it (Asad #2 + Bugbot #478, Medium).
- ensure_cluster_autostart: reset TB_DOCKER_AUTOSTART=0 in the rootless else-branch
(defensive; the is-enabled seed is already guarded off the rootless path) so the
honesty guarantee is local to the branch (Asad #1).
- install_rootless_docker: success line now reads "one or more one-time admin steps"
so it doesn't undercount when both the subuid and cgroup touches happen (Saqlain #1).
- Test: foreign DOCKER_HOST -> warns, no clobber, no double-write. manifest regen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bundle hint (#423) (#476)
* fix(#423): failures surface the real error + log path + support-bundle hint
Fatal errors printed a generic red line while the actionable detail (k3d/helm
stderr) went only to the transcript, and the log path itself was never shown on
screen. Now:
- Err gains an optional $Detail param; Get-ErrDetailLines (pure, unit-tested)
renders the last ~5 non-empty output lines + the log path + a '-Diagnose'
next-step hint, appended to EVERY fatal error.
- Cluster-create failure passes k3d's stdout/stderr so the real reason (image
pull / proxy / port / WSL) shows on screen — the motivating case.
- Helm repo-add / reconcile / install failures pass helm's output via $Detail
instead of embedding it (no more duplicated log-path text).
- Install log path is announced up front in the banner (was log-only before).
Closes#423
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#423): force array enumeration of Err detail lines (Bugbot, defensive)
Bugbot flagged that a single-line Get-ErrDetailLines return (no detail + no
LOG_FILE, e.g. a Confirm-Config failure before Start-InstallLog) unwraps to a
scalar string. The foreach statement already iterates a scalar once (verified:
it prints the whole line, not per-character), so the reported char-splitting
does not reproduce -- but wrap the enumeration in @(...) to make that
unambiguous and future-proof. Adds a regression test asserting the single-line
case stays one intact line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#423): strip PS 5.1 ErrorRecord chrome from the failure excerpt (Bugbot)
helm failures arrive as `native 2>&1 | Out-String`; on Windows PowerShell 5.1
that wraps stderr in ErrorRecord chrome (the `At <file>:<n> char:<n>` position
line plus the `+ ...` / `+ CategoryInfo` / `+ FullyQualifiedErrorId` block).
Get-ErrDetailLines kept only the last 5 non-empty lines, so the excerpt was all
chrome and the real `Error:` line dropped out -- a regression from the previous
full-message dump. Filter those chrome lines before taking the window so the
actual error survives. Adds a regression test simulating the 5.1 rendering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#423): drop duplicate inline log-path hints (Bugbot)
Err now always prints the log path via Get-ErrDetailLines, so the k3d
spawn-failure and create-timeout paths that still Hint "Full log:" right before
Err printed it twice. Remove those inline hints; Err is the single source. Adds
a guard test asserting no inline 'Full log:' hints remain in the installer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#423): put stderr last in the create-failure Err detail (Asad)
Get-ErrDetailLines keeps the LAST 5 non-empty lines, so with detail ordered
stderr-then-stdout any k3d stdout tail could crowd the real stderr reason
(FATA/x509/port) out of the excerpt. Order it stdout-then-stderr so the stderr
tail survives the window; also matches the Write-HostCaCreateHint order just
above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ws installer (#422) (#477)
* chore(#422): honest step labels + per-tool heartbeat/progress in the PS installer
Step 1/5 "Checking system requirements" actually installed ~700 MB of tools,
nearly all console-silent (downloads with the progress overlay off since #471,
plus silent winget/Add-AppxPackage/installer invocations), which reads as a hang.
The k3d start path also streamed raw INFO[...] lines past the style system.
- Split Step 1 into "Checking system requirements" (preflight/GPU/virtualisation)
and a dedicated "Installing system tools" step; renumber to /6.
- Invoke-WithHeartbeat: run a blocking op in a background job with a live spinner
(built on the existing Wait-JobWithProgress) so no op sits silent >10s. Wired
into every tool download (kubectl/k3d/helm/winget/Docker Desktop), the winget
installs, Add-AppxPackage, and the Docker Desktop installer.
- Get-ToolSummaryLine: one honest line per tool (name, version, size, elapsed),
printed as each tool becomes ready.
- Route `k3d cluster start` through Invoke-WithHeartbeat: capture its raw output
to the log + show a styled heartbeat instead of streaming INFO[...] lines
(and fail loudly if start fails, instead of always reporting "started").
- Tests: Pester for Get-ToolSummaryLine, Invoke-WithHeartbeat, and source guards
for the 6-step split + no-raw-k3d-output.
The copy catalog is bash-driven and the bash installer already splits check
(step a) from install (step b) with real progress, so its golden is unaffected.
Closes#422
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#422): job-runspace TLS 1.2 floor + k3d start exit-code check (Bugbot)
Two High-severity findings from moving work into Start-Job via Invoke-WithHeartbeat:
- TLS 1.2 doesn't carry into job runspaces (PS 5.1 defaults to TLS 1.0/1.1), so
in-job HTTPS downloads (kubectl/k3d/helm/winget/Docker Desktop) could fail
SSL/TLS on hosts that need the explicit floor. Re-apply Tls12 in $script:JobInit
(OR-in, don't clobber), which every job runs before its scriptblock.
- A native `k3d cluster start` non-zero exit leaves the job state 'Completed', so
Invoke-WithHeartbeat never threw and the installer reported "Compute environment
started." on a stopped cluster. The start scriptblock now checks $LASTEXITCODE
and throws its captured output, so the existing catch surfaces a real Err.
Adds a functional in-job-TLS test and a source guard for the exit-code throw.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#422): surface heartbeat failure detail + fail loudly on Docker install (Bugbot)
Two follow-on findings from the Start-Job/heartbeat design:
- Invoke-WithHeartbeat threw a generic 'Failed while: ...' and swallowed the
job's real error (Receive-Job -ErrorAction SilentlyContinue), so the k3d-start
detail never reached the log/Err. Now capture output+error (2>&1) and the job's
terminating reason, and include it in the throw; the k3d-start catch passes it
as Err detail too.
- The Docker Desktop installer Start-Process had no -ErrorAction Stop and no exit
check, so a spawn/install failure completed the job as success and Step 2
continued. Now -ErrorAction Stop + PassThru + exit-code throw, wrapped so it
Errs cleanly with the real detail.
Adds a heartbeat failure-detail test + a Docker-installer source guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#422): print k3d/helm summary only after the execute-gate (Bugbot)
k3d and helm printed their green Get-ToolSummaryLine 'ready' line inside the
download branch, before Assert-ToolRuns — so a corrupt/wrong-arch binary showed
as ready and then failed the gate (kubectl already gates first). Compute the
summary at download time (correct elapsed) but defer the Ok until after the
execute-gate passes. Adds a source guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#422): winget Docker install falls back + fails loudly (Bugbot)
The winget Docker path soft-logged failures, never checked $LASTEXITCODE, and
had no direct-download fallback when winget was present — so a failed winget
install let Step 2 continue and only surfaced as the 10-minute Docker-wait
timeout later. Now: the winget scriptblock throws on a non-zero exit; if winget
is absent OR didn't land the exe, fall through to the direct download (parity
with k3d/helm); and a final Test-Path guard Errs immediately if neither path
installed Docker. Adds a source guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#422): run installers as killable processes, not orphan-prone jobs (Bugbot)
Start-Process -Wait / winget install inside Invoke-WithHeartbeat (a background
job) leaks the child process on timeout: Stop-Job ends the job runspace but the
installer keeps running, and the winget path could time out then fall through to
a second concurrent install. Switch the Docker Desktop installer + all winget
installs (Docker, k3d, helm) to Start-Process -PassThru + Wait-ProcessWithDeadline,
which shows the spinner AND kills the actual process on timeout, then checks the
exit code. Downloads (Invoke-WebRequest) stay on Invoke-WithHeartbeat — no child
process to orphan. Updates the Docker source guards accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#422): run k3d cluster start as a killable process too (Bugbot)
Same orphan hazard as the installers: k3d cluster start ran inside
Invoke-WithHeartbeat (a job), so Stop-Job on timeout left the native k3d child
running. Switch it to Start-Process -PassThru + Wait-ProcessWithDeadline (kills
on timeout), redirecting its raw INFO[...] to temp files for the log; check both
the deadline and the exit code so a failed/stuck start Errs with the real reason
instead of a false 'started'. Now every process-spawning op is killable; only
in-runspace downloads + Add-AppxPackage remain on the job-based heartbeat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…omise (#481)
Bugbot on the staging promotion (#480): the Tier-1 branch printed
'no administrator rights needed' BEFORE _ensure_subid_ranges /
_ensure_cgroup_delegation ran - on hosts where either fires, the
operator saw a no-admin promise immediately contradicted by an
announced sudo touch or a prepare-host handoff.
The header now stays neutral ('user-space install'); the two
prerequisite helpers already announce themselves or hand off when
they actually apply. Tier 0's claim is unconditionally true and
stays. Manifest regenerated.
…ce (#417) (#483)
* fix(#417): report host RAM consistently + achievable memory advice
The preflight memory check preferred Docker's WSL2 VM budget over physical RAM,
so the same 15 GB laptop reported "7 GB" with Docker up and "15 GB" with it down
-- flip-flopping across re-runs -- and recommended "give Docker >= 16 GB" on a
15 GB host (impossible).
- Get-PfMemGb now returns HOST RAM only (physical, via CIM) -- identical whether
Docker is up or down. The runtime VM budget is read separately (Get-PfRuntimeMemGb)
and shown as its own labeled line ("Docker's current share: N GB").
- Get-PfMemRecommendation caps every suggestion at (host - 2 GB), so we never
advise more memory than the machine physically has; floors at 1 GB.
- Step-1 (Test-Preflight) and Step-2 (Test-PreflightRuntimeMem) now give one
consistent, host-aware message; Step-2's recommendation is capped too.
Tests: Get-PfMemRecommendation (cap/floor/16-on-15 cases), Get-PfMemGb reports
host RAM regardless of the Docker budget (Windows + cross-platform decoupling),
and Test-PreflightRuntimeMem caps its recommendation at host RAM. Updated the
former "Get-PfMemGb prefers docker" test (it asserted the flip-flop bug).
Closes#417
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#417): don't dangle unachievable memory advice on too-small hosts (Bugbot)
Two follow-ups to the capped-recommendation logic:
- Step-1's middle branch (host below the training threshold) told 5-7 GB hosts to
give Docker host-2 GB (3-5 GB) 'to train locally' — which can't train (~8 GB/job).
It now states the truth: runs fine, but local training needs a bigger machine
(~warnMemGb+2 GB+), with no impossible target.
- Test-PreflightRuntimeMem said 'Raise Docker to N' even when N <= the current
budget (a no-op on a host already at its achievable cap). It now only recommends
raising when that's actually possible; otherwise it names the real fix (more RAM).
Tests updated: the capped-rec test uses a 9 GB host (cap 7, not the 8 target), and
a new test asserts no no-op 'raise to' when already at the cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#417): training-warn threshold accounts for the OS reserve (Bugbot)
Step-1 marked memory Ok at host >= warnMemGb (8), but sparing an 8 GB Docker
budget also needs ~2 GB for the OS (the cap in Get-PfMemRecommendation), so an
8-9 GB host got a green check that Step-2 then contradicted with 'can't spare
more'. Extend the too-small-for-training branch to host < warnMemGb + 2 so
Step-1 agrees with Step-2. Adds a test that a 9 GB host is flagged, not Ok.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#417): make the host-RAM decoupling test host-independent (Bugbot)
The cross-platform decoupling test asserted Get-PfMemGb -Not -Be 8 while mocking
docker to 8 GiB but not CIM, so on a real 8 GB Windows host (where host RAM is
genuinely 8) it would flakily fail even though the fix is correct. Assert instead
that Get-PfMemGb never invokes docker (Should -Invoke docker -Times 0) - the true
decoupling guarantee, host-independent - and add a separate positive test that
Get-PfRuntimeMemGb still follows the docker budget. The exact host figure stays
locked by the Windows-gated CIM-mocked sibling test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#417): grade the effective memory figure, keep host RAM as the label (Asad)
Reworked per review: Step-1 graded host RAM and demoted the Docker budget to a
decorative string, so a throttled budget (e.g. 32 GB host / 2 GB Docker) showed a
green Ok and the 15/7 machine from #417 lost its warning. New Show-MemoryStatus
(shared by Step-1 and the post-Docker re-check):
- Grades the EFFECTIVE figure the client actually gets (Docker's VM budget when
known, else host RAM), so a throttled budget is never green-OK'd; both the min
'will OOM' and warn 'training may OOM' floors apply to the budget.
- Always REPORTS host RAM as the label (no flip-flop); when host RAM is unreadable
(CIM blocked) but the budget is, reports the budget labelled as Docker's share
instead of skipping.
- Threads recMemGb back into the training target (was dead on Windows), capped at
host - OS reserve, so the number is achievable (13 on a 15 GB host, not 10/16).
- Single $script:PfOsReserveGb constant (was the literal 2 in three places); the
warnMemGb+2 rung is gone, so PF_WARN_MEM_GB no longer means two things by OS.
Tests: comprehensive Show-MemoryStatus grading (reviewer's 32/2, 16/4, 15/7, 10/5,
host-down, CIM-blocked, healthy) + Step-1/Step-2 delegation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#417): don't cap memory advice at the throttled budget when host RAM is unknown (Bugbot)
When CIM was blocked (host RAM unreadable), $capHost fell back to the Docker
budget, so recommendations were capped at (budget - reserve) -- producing
backwards, contradictory hints like 'Give Docker at least 5 GB (up to 2 GB)' on a
4 GB budget. The budget is the current throttled value, not a ceiling. Now only
cap at the host when host RAM is known; when it isn't, advise the raw targets
(at least minMemGb, up to warnMemGb). Adds a regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…d#1303) (#486)
Backlog at zero fleet-wide; the quality contexts are already required on
develop. Also adds a workflow_dispatch(all-files) trigger for whole-tree
scans (gitleaks baseline).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…en current (#414) (#484)
* fix(#414): WSL update survives Store-blocked networks + skips when current
The installer ran `wsl --update` through the Microsoft Store with a 90s silent-
timeout job: on Store-blocked corporate networks it silently skipped (Docker
Desktop then confronted the user with its own install-WSL prompt + reboot), it
re-ran up to 90s on every re-run even when the kernel was current, and its
output went only to the log.
New Update-Wsl:
- Skips when WSL is already current (Test-WslCurrent parses `wsl --version`), so
the block finishes in <2s on a re-run.
- Uses `wsl --update --web-download`, which fetches from Microsoft's servers
instead of the Store, so a Store-blocked machine still updates the kernel with
no Docker Desktop WSL prompt. Runs as a killable tracked process with a deadline.
- On failure, surfaces the exact manual MSI step on screen (github.com/microsoft/
WSL/releases), not swallowed to the log.
Scope note: the issue also suggested auto-falling-back to the GitHub-releases MSI.
That isn't implemented automatically because it would require api.github.com (the
WSL asset name carries a 4th version component the API-free /releases/latest
redirect can't resolve), and #410 -- enforced by a test -- forbids the rate-limited
GitHub API in this installer. The manual step is surfaced clearly instead; a test
guards against a regression that re-adds the API.
Tests: Test-WslCurrent parsing; source guards for --web-download, skip-when-current,
no bare Store-path job, the manual step, and the #410 no-API invariant.
Closes#414
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#414): decode wsl --version as UTF-16 so skip-when-current fires (Bugbot)
wsl.exe writes UTF-16LE; capturing it via 'cmd /c ... | Out-String' left the
output null-interleaved, so Test-WslCurrent never matched -- skip-when-current
never fired and every re-run attempted a full (up to 5 min) web update and could
show a false MSI warning. Capture wsl --version with [Console]::OutputEncoding set
to Unicode (the same pattern the wsl --list reader already uses), restored in a
finally. Adds a source guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#414): name the arch-matched WSL MSI in the manual hint (Bugbot)
The manual fallback hint hardcoded wsl.<version>.x64.msi, but Get-WindowsArch
returns arm64 on ARM hosts and GitHub ships wsl.<version>.arm64.msi. An ARM
operator following the x64 step installs the wrong package and still hits the
Docker Desktop WSL prompt this path avoids. Compute the MSI arch from the host.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#414): detect WSL via the version number, not the localized label (Bugbot)
Test-WslCurrent matched the English 'WSL version:' label, but wsl --version
localizes it (e.g. Japanese 'WSL バージョン:'), so skip-when-current never fired on
non-English Windows and every re-run attempted the full web update. Match the
dotted version number instead, which modern WSL always prints regardless of
locale. Adds a non-English test case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#414): harden WSL update per review — floor, bounded probe, retry, real errors
Reworked Update-Wsl to address Asad's review:
- Test-WslCurrent now grades a version FLOOR, not mere presence: it pulls the
first dotted version (the WSL version line, locale-independent) and requires
>= TB_WSL_MIN_VERSION (default 2.1.0), so a stale modern WSL (2.0.x) still
updates instead of being green-OK'd forever.
- The wsl --version probe is BOUNDED: Get-WslVersionOutput runs it in a job with
Wait-JobWithProgress -TimeoutSec 20 (like the wsl --list reader) and returns ""
on timeout, so a wedged LxssManager can't freeze Step 1. The encoding restore is
wrapped (finally { try {...} catch {} }) so it can't kill the installer on a
console-less host.
- Invoke-WslUpdate runs wsl --update as a tracked process with a deadline,
redirects stdout/stderr to temp files (logged), and classifies the outcome
(ok / not-found / timeout / failed) — so failures leave real WSL evidence in the
log + -Diagnose, and wsl's \r progress no longer fights the spinner.
- Two-rung ladder: on a non-zero web-download exit (unpatched wsl.exe rejects the
flag), retry plain `wsl --update` before giving up.
- Differentiated failure messages: not-found / timed out / exited N — no longer
the single "the Store may be blocked" line that --web-download rules out.
Tests: Update-Wsl is now EXECUTED (mocked deps) across skip / web-download / retry
/ timeout / not-found branches; Test-WslCurrent covers the stale-floor + custom-
floor cases; source guards anchored on the real invocations; dropped the duplicate
#410 guard (the #410 Describe owns that invariant).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#414): raise WSL currency floor to Docker Desktop's 2.1.5 minimum (Bugbot)
The 2.1.0 floor let 2.1.0-2.1.4 boxes skip the update yet still hit Docker
Desktop's update-WSL prompt (it requires >= 2.1.5). Default the floor to 2.1.5.
Adds a 2.1.4 boundary test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…22) (#485)
* feat(install): Tier-1 no-systemd fallback + Tier-2 fall-through (#1222)
Last slice of #1177 (LPI Tier 1) — the code hardening that completes the rootless
path. Everything stays behind the opt-in TB_TIER1_ROOTLESS flag; the §5 host-matrix
validation (fuse-overlayfs perf) and the flag flip to default-on are host-gated and
NOT in this PR (deferred, tracked on #1222).
- _user_systemd_available: detect a usable per-user systemd manager via
`systemctl --user is-system-running` (a state word => present, even on non-zero
exit; empty => no manager/bus) plus XDG_RUNTIME_DIR.
- _start_rootless_nohup: on hardened/HPC nodes with no user-systemd, start
dockerd-rootless.sh via nohup under an owned XDG_RUNTIME_DIR, poll the socket to
Ready, skip linger. Still user-space, no root. Sets TB_ROOTLESS_NO_LINGER.
- install_rootless_docker branches systemd-vs-nohup; the daemon-verify failure now
routes via _tier2_fallthrough (prepare-host remedy) instead of a bare error — no
proceeding on a broken socket, no false Tier-1.
- summary.sh::_reboot_note: honest "will NOT restart automatically" note on the
no-linger path (takes precedence over the autostart flag).
Tests: no-systemd nohup branch; daemon-never-Ready -> Tier-2 fall-through; the 5
existing install_rootless_docker tests updated to model is-system-running; the
reboot-note no-linger case. shellcheck clean; full bats suite green; manifest regen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): address Bugbot #485 — persist the exact rootless runtime dir (no-systemd path)
On the nohup fallback, /run/user/<uid> may be unwritable so the socket lands under
$HOME/.tracebloc-rootless-run. Before, the persisted DOCKER_HOST used the generic
${XDG_RUNTIME_DIR:-/run/user/$(id -u)} template (→ wrong socket in a fresh no-systemd
shell) and the restart guidance omitted XDG_RUNTIME_DIR (dockerd-rootless.sh refuses
without it), so the operator couldn't bring the daemon back. Now:
- _start_rootless_nohup records TB_ROOTLESS_RUNTIME_DIR and shows the full
'XDG_RUNTIME_DIR=<dir> nohup dockerd-rootless.sh &' restart command.
- _persist_docker_host persists 'export XDG_RUNTIME_DIR=<dir>' before DOCKER_HOST, so a
new shell resolves the SAME socket the install used AND can restart the daemon.
- summary.sh::_reboot_note carries the exact dir in the restart hint.
- Tests: rc sourced with XDG unset resolves DOCKER_HOST to the $HOME socket; the runtime
dir is recorded; the reboot-note hint carries the dir. manifest regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): address Bugbot #485 r2 — honest 'Started' claim + setuptool Tier-2 fall-through
- _start_rootless_nohup: only claim "Started rootless Docker…" once the poll confirms the
daemon answered (_up). A bare "Started…" before a failed poll contradicted the shared
verify's "daemon never answered" fall-through moments later (Bugbot medium).
- install_rootless_docker: guard both install paths (dockerd-rootless-setuptool.sh /
get.docker.com/rootless) with '|| _tier2_fallthrough', so a setuptool/installer failure
routes to the prepare-host remedy instead of a bare set -e abort with the spinner log
tail — _tier2_fallthrough's documented setuptool coverage was not actually wired (Bugbot medium).
- Tests: nohup daemon-never-answers => no false "Started" + Tier-2; setuptool install failure
=> Tier-2 fall-through naming the setuptool. manifest regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): address Bugbot #485 r3 — don't clobber a session XDG_RUNTIME_DIR
The r1 persist wrote 'export XDG_RUNTIME_DIR=<dir>' unconditionally into the shell rc.
~/.bashrc is sourced on every host sharing the home (HPC NFS), so that clobbered a
legitimate pam/systemd /run/user/<uid> on a systemd node and broke user-systemd there —
a regression from the r1 fix. Guard it: 'export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-<dir>}"',
supplying our dir only when the session hasn't set one. The test now also asserts a
pre-set XDG is preserved (not clobbered) alongside the no-systemd resolve case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): address Bugbot #485 r4 — holistic rewrite of the no-systemd persist/launch path
- _launch_dockerd_rootless: add </dev/null so the backgrounded daemon can't inherit the
installer's `curl | bash` pipe stdin and consume the rest of the script (Bugbot High).
- _persist_docker_host: rewrite as an atomic BEGIN/END managed block, stripped + re-appended
each run. The prior per-line append landed a re-run's XDG line AFTER DOCKER_HOST, so it
never took effect (Bugbot medium). The runtime dir is now baked into the DOCKER_HOST
fallback (order-independent resolution); the guarded ${XDG_RUNTIME_DIR:-…} line supplies
it for the daemon restart without clobbering a systemd node's /run/user/<uid>.
- Self-review hardening: same-dir temp + `cat` (not `mv`) so a symlinked/stow'd rc + perms
survive and a full disk bails before touching the rc; strip only a WELL-FORMED block (both
markers) so a malformed rc isn't eaten past a missing END marker.
- Tests: systemd→nohup transition; </dev/null guard; unrelated-content/malformed-block safety.
full setup-linux + summary suites green; shellcheck clean; manifest regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(install): descope the no-systemd nohup fallback from #1222 -> Tier-2 (#1354)
Six consecutive Bugbot rounds landed on the no-systemd nohup fallback (async daemon +
set -e + curl|bash stdin + shared-home rc persistence), none validatable without a real
HPC host. Descope it: a host with no per-user systemd now routes to the Tier-2 prepare-host
remedy (honest + testable) instead of a blind nohup bring-up.
- Delete _start_rootless_nohup + _launch_dockerd_rootless; install_rootless_docker's
no-systemd branch now calls _tier2_fallthrough.
- Revert _persist_docker_host to the simple systemd-path form (pam sets XDG_RUNTIME_DIR;
no $HOME-fallback / atomic-block / XDG-persist complexity).
- Drop the now-dead TB_ROOTLESS_NO_LINGER branch in summary.sh::_reboot_note.
- Tests: no-systemd => Tier-2 fall-through; removed the nohup / persist-XDG / launch tests.
full setup-linux + summary suites green; shellcheck clean; manifest regenerated.
The nohup fallback is tracked for a host-available slice in #1354.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): gate on user-systemd BEFORE installing (Bugbot #485)
install_rootless_docker checked _user_systemd_available only AFTER the setuptool install
+ the user proxy drop-in. The setuptool sets up a `systemctl --user` unit and fails first
on a no-systemd host, so the operator got a vague setuptool reason plus a partial ~/bin
install + drop-ins before the Tier-2 remedy. Move the gate to the TOP -> fail fast to
_tier2_fallthrough with the accurate "no per-user systemd" reason and no artifacts. The
later systemd branch is now unconditional (the redundant re-check is removed). Test now
also asserts the setuptool never runs on the no-systemd path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(install): name the researcher in _tier2_fallthrough's prepare-host remedy (Bugbot #485)
_tier2_fallthrough printed a bare `prepare-host` hint with no TB_PREPARE_USER / username.
run_prepare_host only grants docker-group access + provisions subuid ranges when the user
is named, so an admin who followed the bare hint prepared the host but NOT the researcher —
looping them back into the same fall-through. Name the researcher (id -un), matching
_ensure_subid_ranges' hand-off verbatim (export TB_PREPARE_USER=<user>, `prepare-host <user>`).
Test asserts the remedy names them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…end (#419) (#487)
* feat(#419): install Docker Desktop unattended with the WSL2 backend
On the winget path the installer couldn't pass Docker Desktop's own installer
args (winget's manifest defaults decided), leaving first-launch license/onboarding
prompts possible and the backend implicit; the direct path passed
--quiet --accept-license but not --backend=wsl-2.
- winget: add --override 'install --quiet --accept-license --backend=wsl-2
--always-run-service' so Docker's installer flags apply through winget.
- direct: add --backend=wsl-2 --always-run-service to match.
- --always-run-service (Docker's documented unattended flag) starts the engine
service without a GUI first-run, so a fresh machine reaches a running engine
with zero Docker Desktop interaction.
Tests: source guards that both paths select the WSL2 backend + run the service
unattended, and that winget uses --override.
Closes#419
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#419): pass winget --override as a single quoted string (PS 5.1 safe, Bugbot)
The --override value was one ArgumentList array element containing spaces; PS 5.1's
Start-Process joins array elements without quoting, so winget received
'--override install' plus stray --quiet/--accept-license tokens and the Docker
installer flags never applied (winget failed -> fell back to the 600 MB direct
download). Build the winget args as a single command-line string with the override
value explicitly double-quoted, so it reaches winget as one argument. Updated the
source guards accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…e docs (#421) (#489)
* feat(#421): self-elevate an un-elevated PowerShell run + cmd-safe docs
Real users paste the one-liner into a normal PowerShell (or cmd) and the install
either dies on the admin requirement or errors syntactically. #386 added
elevation instructions; this automates the common case.
- On an un-elevated interactive run, offer to relaunch elevated: one consent ->
Start-Process -Verb RunAs (UAC) -> install proceeds. Get-ElevationCommand builds
the relaunch args -- re-run the on-disk .ps1 when present, else re-fetch the
one-liner (irm|iex) -- forwarding -NoReboot/-Diagnose. Non-interactive/declined/
failed falls back to the followable Win+X -> Terminal (Admin) steps (#386).
- docs/INSTALL.md: add the cmd-safe form
(powershell -ExecutionPolicy Bypass -Command "irm ... | iex") so a paste into
cmd.exe runs instead of a syntax error, plus the exact Win11 admin steps.
Env-var config (TRACEBLOC_*) is intentionally not forwarded across RunAs (no env
inheritance; secrets on a command line are unsafe) -- documented; env-driven runs
should be launched elevated.
Tests: Get-ElevationCommand (file vs one-liner, switch forwarding) + gate source
guards. Website snippet + screenshot are a separate repo (noted in the PR).
Closes#421
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#421): elevation re-fetches the one-liner for temp-dir runs + forwards switches (Bugbot)
Two elevation-relaunch defects:
- The documented irm|iex flow runs install-k8s.ps1 from a bootstrap TEMP dir the
un-elevated process deletes on exit, so -File <tempfile> in the elevated window
hit a missing script. Only use -File for a DURABLE (non-temp) path; otherwise
re-fetch the one-liner.
- The one-liner branch dropped -NoReboot/-Diagnose. iex can't take args, so when
switches must be forwarded, invoke the fetched shim as a scriptblock with them;
keep the plain irm|iex form when there are none.
Adds temp-dir and switch-forwarding tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#421): quoted command-line string + drop unforwardable one-liner switches (Bugbot)
Two elevation follow-ups:
- Get-ElevationCommand now returns a single command-line STRING and quotes the
-File path, so a script path with spaces survives PS 5.1's Start-Process
-ArgumentList (which doesn't quote array elements; same class as #419).
- The one-liner path no longer tries to forward switches via a scriptblock: the
shim (i.ps1) has no param block, so & ([scriptblock]) -Diagnose fails on an
unknown named parameter; and an irm|iex launch can't have set a switch anyway.
Keep the exact documented irm|iex form.
Tests updated for the string contract + the no-scriptblock one-liner path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…CDN (#497)
* fix(ci): read index.yaml via the contents API, not the raw CDN
verify-index fetched index.yaml from raw.githubusercontent.com seconds
after the release job may have pushed gh-pages. raw.* is CDN-fronted and
serves a stale copy for a while after a push, and ?nocache= does not
reliably bust it -- so the job could read a clean index while the
customer-facing one was already polluted, greening the exact backstop it
exists to be.
Now read through the contents API at ?ref=gh-pages, which is
read-after-write consistent for a ref, and fail loudly on an empty read
rather than reporting the invariants as holding on no data.
Found by Bugbot on the staging->main promotion (client#495).
* fix(ci): emit the empty-read error on stdout so it annotates
Actions parses workflow commands from stdout only, so an ::error:: sent to
stderr fails the step with no annotation. Every sibling ::error:: in this
file already uses stdout (Bugbot, client#497).
…ted window (#418) (#493)
* feat(#418): provision Docker for the daily user during the elevated window
Hospital/enterprise reality: the researcher gets a temporary admin window (or IT
installs), then elevation is revoked -- leaving the standard account unable to use
Docker, a cluster that may not recover, and a 50%-of-RAM VM that OOMs training.
The installer runs elevated, so provision the day-to-day account now.
New Set-DailyUserProvisioning (warn-only; TRACEBLOC_SKIP_DAILY_USER opts out):
- Resolves the daily user (-DailyUser param, else prompt when an admin installs for
someone else, else the current account); Resolve-DailyUser strips the domain.
- net localgroup docker-users <user> /add so the standard account can use Docker.
- Docker Desktop autostart via the per-user Run key (current user). The engine also
runs as a service (--always-run-service, #419), so Docker is usable on sign-in
regardless; cluster nodes already carry restart=unless-stopped (Set-ClusterAutostart).
- Writes a training-sized %UserProfile%\.wslconfig (Get-WslConfigMemoryGb: physical
RAM - 4 GB, floored) for the daily user, preserving an existing tuned file. It
applies at the daily user's next sign-in -- the acceptance scenario -- so we do
NOT wsl --shutdown and tear down the just-built cluster mid-install.
- Prints a "Configured for <user>: ..." summary so IT can review what changed.
Tests: Get-WslConfigMemoryGb (cap/floor), Get-WslConfigContent, Resolve-DailyUser,
plus source guards for docker-users / .wslconfig-preserve / opt-out + wiring.
Closes#418
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#418): address Bugbot round 1 on daily-user provisioning
- .wslconfig merge: Add-WslMemorySetting keeps an existing memory= AND preserves
any other tuning (processors/swap/other sections) instead of overwriting the
whole file. Inserts under an existing [wsl2] header, else appends a section.
- missing profile: when the daily user has never signed in (Get-UserProfileDir
null), note ".wslconfig after first sign-in" in the summary instead of silently
skipping.
- elevation: forward -DailyUser through Get-ElevationCommand / Invoke-SelfElevate
so the choice survives the UAC relaunch.
- input hygiene: sanitize the prompted username via ConvertTo-SanitizedInput
before it reaches net localgroup and profile paths.
Tests: Add-WslMemorySetting (create/keep/insert-preserve/append-preserve),
Get-UserProfileDir null, plus wiring guards for the merge helper, the
no-profile note, the sanitize call, and -DailyUser elevation forwarding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#418): address Bugbot round 2 on docker-users provisioning
- Never hide a docker-users failure behind a green summary (High). Membership is
the make-or-break step; track $dockerUsersOk and, when it doesn't take, WARN
loudly (with the manual `net localgroup ... /add` to run while admin is still
available) even if autostart/.wslconfig succeeded. Green "Configured for" now
only prints when membership is confirmed.
- Verify membership by STATE QUERY instead of string-matching 2>&1-merged native
output (Medium; learned PS rule). New Test-LocalGroupMember (Get-LocalGroupMember,
falling back to `net localgroup <group>` STDOUT) + pure Test-NameInGroupOutput
(domain-stripped, case-insensitive) replace the locale-fragile
`-match 'already a member'` classification.
Tests: Test-NameInGroupOutput (bare-name/domain/case/empty), plus source guards
for the state-query verification and the loud-warn-on-failure summary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#418): note .wslconfig as manual step when host RAM undetectable
Bugbot round 3 (Medium): when Get-PfMemGb returns null the .wslconfig budget was
skipped with no summary note -- unlike the null-profile path -- so a green
"Configured for" could print while the training memory budget was never applied.
Add the matching note ("couldn't detect host RAM -- set [wsl2] memory manually")
so IT sees the gap. Guard test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#418): surface a thrown .wslconfig write in the summary
Bugbot round 4 (Medium): if the .wslconfig merge/write throws (permissions/disk),
the catch only Logged and recorded no summary note, so with docker-users already
confirmed the green "Configured for" line still printed as if the memory budget
was set. Add a "couldn't write .wslconfig -- set [wsl2] memory manually" note in
the catch, matching the null-profile / RAM-unknown paths. Guard test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Inside WSL2 the environment IS Linux, so the existing Linux tiers apply unchanged — no
separate Windows code path. Make the installer WSL-aware:
- probe.sh: _probe_wsl (WSL_DISTRO_NAME/WSL_INTEROP env, or microsoft/WSL in the kernel
release; paths overridable for tests) + PROBE_WSL in run_host_probes (Linux-only,
read-only, never-fatal). Classification is UNCHANGED — a usable Docker (incl. Docker
Desktop's WSL integration, if present) is Tier 0, else Tier 1 rootless.
- render_host_audit: a WSL2 "Environment" row surfacing the rootless-preferred stance;
the Tier-2 unsupported-os message now points Windows users at WSL2 (rootless) as the
preferred path over Docker Desktop.
- docs/INSTALL.md: WSL2 quick-start note — enabling WSL2 as the one-time Windows admin
step + the rootless-over-Docker-Desktop preference with the licensing rationale.
- 8 probe.bats tests; shellcheck clean; manifest regenerated (R8).
End-to-end verification inside a real WSL2 Ubuntu is host-gated (needs a Windows host);
the probe/audit/messaging is unit-tested here.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LukasWodkaand others added 6 commits July 31, 2026 15:45
…n unachievable range (#444)
* fix(installer): memory advice never drops below the client minimum; a too-small host says so (#417 residual)
This PR's ORIGINAL scope is superseded. #483 ("report host RAM consistently +
achievable memory advice (#417)") merged to develop on 2026-07-30 and closed#417, delivering the same Windows half by a different route: `Get-PfMemGb`
returns host RAM only, `Get-PfRuntimeMemGb` is shown as its own labelled line,
`Get-PfMemRecommendation` caps at host − 2 GB, and `Show-MemoryStatus` is the
single copy shared by Step 1 and the post-Docker recheck. Re-landing this
branch's `Get-PfHostMemGb` / `Get-PfMemTargets` / `Write-PfRuntimeMemStatus`
would only rename what already works.
What survives is the finding behind this branch's second commit, which is STILL
LIVE on develop: `Get-PfMemRecommendation` floors at 1 GB, so on a small host it
returns a number below the client's own minimum. Measured on develop:
Show-MemoryStatus -HostGb 6 -BudgetGb 3
⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM.
Give Docker at least 5 GB (up to 4 GB): ... [wsl2] memory=4GB ...
"at least 5 GB (up to 4 GB)" is an empty range, and the concrete value it tells
the operator to write is below the 5 GB the same sentence demands — the warning
cannot be cleared by following the advice. #483's Bugbot pass fixed this only for
the host-RAM-unreadable case; a KNOWN small host still hits it.
- `Get-PfMemRecommendation` now floors at PF_MIN_MEM_GB instead of 1, matching
bash's `_pf_clamp_mem_gb` exactly so both installers advise the same on the
same hardware.
- `Show-MemoryStatus` treats a host that cannot reach the floor even with the OS
reserve honoured (host − PF_OS_RESERVE_GB < PF_MIN_MEM_GB) as NOT a budget
bottleneck, so it gets the honest "use a larger machine" line rather than a
resize remedy it can never satisfy. This mirrors the bash recheck's
host-too-small branch, and the sibling fix in #445.
After:
Show-MemoryStatus -HostGb 6 -BudgetGb 3
⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM.
This machine has 6 GB of RAM total; the client needs at least 5 GB. Free up memory or use a larger machine.
Hosts that can reach the floor are unchanged (8 GB host still offers the resize;
15 GB → 13 and 16 GB → 14 recommendations are untouched).
Tests: 7 new/updated — the floor is the client minimum not 1, a 6 GB host never
yields a sub-floor number, PF_MIN_MEM_GB overrides the floor, an invariant sweep
over hosts 1..24 GB, the too-small host gets "larger machine" with no memory=1-4GB
value, and a host that CAN reach the floor still gets the resize hint. The former
"floors at 1 GB" assertion is replaced — it pinned the bug. Pester 374 passed /
0 failed / 9 skipped; PSScriptAnalyzer 0 errors; check-style, check-drift and
gen-manifest --check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(installer): name the OS reserve in the host-too-small hint (Bugbot #444)
The previous commit routed 5-6 GB hosts into `Show-MemoryStatus`'s generic
too-small hint, which compares total RAM to the floor and stops there:
host=6 GB, budget=3
⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM.
This machine has 6 GB of RAM total; the client needs at least 5 GB. Free up memory or use a larger machine.
6 >= 5, so as written the operator is told they have enough and still need a
bigger machine. The shortfall only adds up once the ~2 GB the OS needs is named —
which is exactly what bash's `_pf_recheck_runtime_mem` already says. That hint
predates this PR, but this PR is what made 5-6 GB hosts reach it, so it fixes it.
host=6 GB, budget=3 # after
⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM.
This machine has 6 GB of RAM total - too little for tracebloc: the client needs a 5 GB Docker budget and the OS needs ~2 GB, so about 7 GB physical is the practical minimum. Use a larger machine.
Hosts that can reach the floor are untouched (8 GB still gets the resize remedy;
16 GB still gets the training recommendation), and a host below the floor outright
keeps the plain copy.
Tests: 3 new — the reserve and the practical minimum are both named, and the
arithmetic is explained for every too-small host (4-6 GB). Pester 376 passed /
0 failed / 9 skipped; PSScriptAnalyzer 0 errors; parse, check-style, check-drift
and gen-manifest --check all clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(installer): the too-small-host guard also covers the training branch (Bugbot #444 r2)
`hostTooSmall` was consulted only inside the below-floor branch, so a 5-6 GB host
with Docker DOWN graded as "enough to run" and fell into the TRAINING branch,
which printed a concrete budget to write:
host=6 GB, budget=<none>
⚠ Memory: 6 GB - enough to run the client, but training (~8 GB/job) may OOM; 5 GB recommended to train locally.
For local training, give Docker up to 5 GB: WSL2 backend - [wsl2] memory=5GB ...
memory=5GB on a 6 GB machine leaves the OS 1 GB — a budget this same function
calls unachievable two branches up. The previous commit's floor change is what
lifted that number from 4 to 5 and made it reachable, so this closes the hole it
opened rather than trading one inconsistency for another.
Such a machine cannot be tuned into a training box at all, so the branch now says
that instead of printing a number:
host=6 GB, budget=<none> # after
⚠ Memory: 6 GB - enough to run the client, but too little to train locally (~8 GB/job).
This machine has 6 GB of RAM total and the OS needs ~2 GB, so it cannot give Docker a training-sized budget. Run the client here and train on a larger machine.
Machines that CAN reach the floor keep the actionable number (16 GB host with a
6 GB budget still gets "give Docker up to 14 GB" / memory=14GB), and the healthy
paths are untouched.
Tests: 4 new, including the invariant that closes this class for good — across
every branch and every budget shape (host 1-6 GB x budget none/1-6), no branch may
emit a concrete memory= value for a host that cannot reach the floor while keeping
the OS reserve. Pester 379 passed / 0 failed / 9 skipped; PSScriptAnalyzer
0 errors; parse, check-style, check-drift, gen-manifest --check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… proceed (#520)
* fix(installer): Windows was the one OS that let a sub-floor Docker VM proceed
#513 decided a Docker VM below the client's memory floor must STOP the install
rather than proceed and OOM-crashloop -- "proceeding is worse than the jarring
stop the WARN path used to avoid" -- and implemented that in bash for every OS
(_pf_recheck_runtime_mem -> error -> exit 1). The Windows installer never got it.
Test-PreflightRuntimeMem just called Show-MemoryStatus, which is warn-only, so
Windows printed "it will OOM" and then carried on and OOM-crashlooped. The
platform this whole memory story (#417/#418/#428/#444/#516) is about was the one
platform still shipping the crash.
Enforcement now lives in Test-PreflightRuntimeMem, mirroring bash's split:
Show-MemoryStatus stays purely presentational (its documented job -- and the
function two PRs just contended over), the recheck grades then enforces. It runs
as New-K3dCluster's FIRST statement, so exiting leaves no half-built cluster.
The subtlety that makes this safe: Get-PfRuntimeMemGb floors to whole GB, and a
guest reports a few hundred MiB BELOW its configured size, so a VM set to exactly
the documented 5 GB floor reports ~4.8 and floors to 4. A bare `-lt 5` would have
hard-failed a correctly configured machine -- the same trap #513's reviewer caught
in bash, but worse here because flooring to whole GB discards up to a GB. So the
gate compares MiB against floor - grace:
- New Get-PfRuntimeMemMib: the same `docker info` value at MiB precision.
- New Get-PfVmMemGraceMib (512, PF_VM_MEM_GRACE_MIB) -- the same constant and the
same comparison bash uses, so both installers put the floor in the same place.
- The recheck now reads the budget ONCE, in MiB, and derives GB from it, so the
number printed and the number enforced on cannot disagree. Flooring (not
rounding) is kept deliberately: Step-1 floors too, and #417 exists so the
reported figure doesn't flip-flop between the two reads.
Remedies stay honest and achievable, matching the copy the advice path already
prints: a host that CAN reach the floor gets a resize target clamped to it
(min(warn, physical - reserve) -- bash's clamped warn target); a host that cannot
(physical - reserve < floor) gets the practical minimum and "run the client on a
larger machine", never a resize that repeats an impossible size. A
between-floor-and-warn budget still only warns -- it can run, just tightly.
TRACEBLOC_SKIP_PREFLIGHT still overrides, and Err names it.
Tests: the Describe that asserted warn-only is replaced by the new contract --
sub-floor 4 GB hard-fails; a floor-sized VM reporting 4800 MiB passes; the grace
band is bounded on both sides (4607 fails, 4608 passes); daemon-silent is a
no-op; between-floor-and-warn only warns; the rec is still capped at host RAM;
big-host vs host-too-small remedies; host RAM unreadable still fails; the skip
env overrides; and the budget is read exactly once. Plus a parity Describe that
reads BOTH sources and asserts bash still hard-fails, Windows hard-fails too and
is no longer warn-only, and both name the same grace constant -- so the next
divergence fails a test instead of shipping.
The old tests mocked Get-PfRuntimeMemGb, which this no longer calls; left as-is
they would have passed while testing nothing, so they now mock the MiB reader.
Mutation-tested rather than trusted: neutering the gate back to warn-only fails 4
enforcement tests; restoring passes all 11.
Gates: Invoke-Pester scripts/tests/ -> 401 passed / 0 failed;
Invoke-ScriptAnalyzer as CI scopes it -> 0 errors; check-style.sh clean;
check-drift.sh no drift; manifest.sha256 regenerated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(installer): make the grade and the gate share one boundary (Bugbot)
Bugbot caught a self-contradiction I had reasoned about and wrongly accepted: the
grade was computed from floor($mib / 1024), so a VM configured at exactly the 5 GB
floor (reporting ~4800 MiB) became budget 4, and Show-MemoryStatus printed
hard-floor "it will OOM" copy plus a resize hint -- for a machine the grace-aware
gate immediately ACCEPTED. We told a correctly configured box it would crash and
then carried on. That is precisely the "installer contradicts itself in the same
run" pattern #418/#516 existed to remove; bash classifies that band warn-only.
I had rejected rounding because Step-1 floors and #417 exists so the reported
figure doesn't flip-flop. The fix avoids that trade-off entirely: fold the SAME
grace in before flooring.
$budget = floor(($mib + $grace) / 1024)
- 4800 + 512 -> 5: reports the CONFIGURED size (what the user set and can change),
grades in the warn band, gate passes. Consistent.
- 4096 + 512 -> 4: still sub-floor, still "it will OOM", gate still fails.
Consistent.
Because the grade and the gate now pivot on the same constant, their boundaries
are the same boundary -- (floor * 1024 - grace) MiB. There is no band that warns
"will OOM" yet proceeds, and none that passes while being called sub-floor. The
contradiction is impossible by construction, not merely absent at the values I
happened to test.
Tests: the floor-sized VM is asserted NOT to be told it will OOM and to report
its configured 5 GB; plus a boundary-coincidence test sweeping 4096/4607/4608/
4800/5120 that asserts at EVERY point the copy and the gate agree.
Mutation-tested: reverting the grade to floor($mib / 1024) fails both new tests;
restoring passes all 13.
Gates: Invoke-Pester scripts/tests/ -> 403 passed / 0 failed; Invoke-ScriptAnalyzer
as CI scopes it -> 0 errors; check-style.sh clean; check-drift.sh no drift;
manifest.sha256 regenerated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ange (#522)
Three `producer | early-exiting-consumer` pipelines ran under `set -o pipefail`.
When the consumer exits on its first match/line it closes the pipe, and once the
producer has more than the ~64KB pipe buffer left to write it takes SIGPIPE and
the pipeline exits 141 — a *success* case reported as failure. Measured on
ubuntu-24.04 (bash 5.2.21 / GNU grep 3.11 / coreutils 9.4): 65,622 bytes is
already enough.
WHY this matters most in the chart guard: there the 141 lands on the `if !`
branch, so a genuine `client/templates/**` edit is reported as "guard N/A" and
the version-bump check is SKIPPED — the guard fails OPEN and waves through
exactly the dark ship it was added to stop (PR #472 / the perIngestionTables
flag block). Demonstrated end-to-end on a real repo: at 1,301 changed paths
(58,723 bytes) with a template edit and NO version bump, the old body exits 0
"guard N/A"; the new one exits 1 and blocks. The mirror direction is broken too:
a SIGPIPE on the `grep -qx client/Chart.yaml` MATCH short-circuits the `&&`, so
a PR that DID bump the version is failed with a message that says it did not.
Restructured rather than papered over with `|| true`, which would only convert a
fail-open into a different fail-open:
- chart-version-guard.yml — classify the changed-file list with a bash `read`
loop + `case` (no pipe, no subprocess, so neither SIGPIPE nor a grep rc=2 can
be mistaken for "no match"), and fail CLOSED with ::error:: on a missing base
SHA or a failed `git diff`: "don't know" must never read as "nothing changed".
- _pull_failure_detail — `head -n 3 <<< "$bad"`. With errexit live the old
pipeline aborted the function AT that line, dropping the scoped pull event
underneath it, i.e. the one actionable reason (x509 / blocked registry / auth).
- _gpu_stack_signature — capture whole, take the first line with `%%$'\n'*`. This
site was NOT reachable in practice (the trailing `|| true` already absorbed the
141), but that `|| true` swallowed every real failure code alike; `|| ..._out=""`
states the actual contract (absent tool / timeout ⇒ empty ⇒ don't cache).
Behaviour is unchanged below the buffer threshold: 669/669 bats tests pass, and
_gpu_stack_signature is byte-identical across normal / multi-line / absent /
timeout / 20k-line-chatty probe output. Verified on bash 3.2 (macOS floor) too.
scripts/manifest.sha256 regenerated via scripts/gen-manifest.sh (R8).
Co-authored-by: Claude <noreply@anthropic.com>
…fied path (not bare brew) (#521)
macOS installed the CLI tools with bare `brew install kubectl/k3d/helm`, which
floated to latest and SILENTLY ignored the K3D_VERSION/HELM_VERSION pins — so Macs
ran different, chart-untested tool versions than the pinned Linux installs (and than
the docs claim), with no checksum of our own.
Route macOS through the SAME pinned, checksum-verified direct-download path as Linux.
Both setup-*.sh are always sourced, so install_macos_cli_tools now calls the shared
install_kubectl/install_k3d/install_helm (setup-linux.sh) after setting OS_DL=darwin
and a macOS tools target (/usr/local/bin, on the default PATH on Intel + Apple
Silicon). The fetchers are made OS-aware via ${OS_DL:-linux} — Linux (and every bats
fetch test that leaves OS_DL unset) stays byte-identical.
- common.sh: portable _verify_sha256 — GNU sha256sum on Linux, shasum -a 256 on
macOS (which ships a BSD /sbin/sha256sum that lacks GNU --check). A `type -t`
guard honors the bats mocks' sha256sum shell-function so the Linux fetch tests
keep passing on macOS dev boxes.
- Execute-gate (#411) is preserved: each shared installer ends in assert_tool_runs,
so a broken/wrong-arch binary fails the "System tools" step loudly. Updated the
drift-check contract to accept macOS delegating to the gated install_<tool>.
- brew still delivers Docker Desktop / colima (install_docker_desktop) unchanged.
Tests: new scripts/tests/setup-macos.bats (OS_DL=darwin fetch/verify for all three
tools + _verify_sha256 portability + install_macos_cli_tools delegation, no bare
brew); +2 check-drift self-tests for the delegation contract. shellcheck/style/drift
clean; manifest regenerated.
Closes#429
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…h VZ/Rosetta (#524)
* fix(#433): verify amd64 emulation on Apple Silicon + start colima with VZ/Rosetta
The client images are amd64-only. On Apple Silicon the installer merely printed a
note and proceeded, ASSUMING Docker Desktop's emulation works — so if "Use Rosetta
for x86_64/amd64 emulation" is off (or colima lacks it), the images crash-loop with
an exec-format error minutes later, with no preflight/setup catch. The headless
colima path was worse: `colima start` passed no arch/Rosetta flags, so an Apple
Silicon Mac got an arm64 VM running amd64 images under slow QEMU or not at all.
- assert_amd64_emulation (setup-macos.sh): post-Docker smoke — force-run a tiny amd64
binary (`docker run --rm --platform linux/amd64 busybox:1.36 true`) once Docker is
up, and HARD-FAIL naming the exact Docker Desktop setting + the colima remedy, so
the problem is caught at setup, never as a crash-looping pod. Wired into install_macos
right after Docker is confirmed ready. Intel Macs skip it (native amd64);
TRACEBLOC_ALLOW_ARM64 is the escape hatch; image overridable via TB_AMD64_SMOKE_IMAGE.
- _install_docker_colima: on Apple Silicon + macOS 13+ (VZ), start colima with
`--vm-type vz --vz-rosetta` for Rosetta-accelerated amd64 (matches Docker Desktop's
Rosetta setting); older macOS keeps the QEMU default. bash-3.2-safe: the arg vector
is never empty. New _macos_supports_vz helper (TB_MACOS_VER-overridable).
- _pf_arch (preflight.sh): the macOS note now NAMES the Rosetta setting and says the
real check runs once Docker is up — instead of "assume it works".
Tests: new scripts/tests/setup-macos-arch.bats (VZ detection, colima flag matrix,
smoke pass/fail/skip/override) + a preflight.bats assertion on the named setting.
Separate test file from setup-macos.bats to avoid a file-add clash with #429/#521.
shellcheck/style/drift clean; manifest regenerated; preflight.bats (82) green.
Closes#433
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#433): guard colima VZ flags on existing VMs + time-bound the amd64 smoke (Bugbot)
Two Bugbot findings on the Apple Silicon work:
1. (High) colima refuses to change vmType on an EXISTING instance, so unconditionally
appending --vm-type vz --vz-rosetta aborted `colima start` on a prior QEMU VM (from
an earlier install or reboot) with a generic failure. Only request VZ+Rosetta on a
FRESH start now (new _colima_instance_exists via `colima list --json`); a pre-existing
VM starts as-is, and if its amd64 emulation is broken the post-Docker smoke already
names the `colima delete && colima start --vm-type vz --vz-rosetta` recreate remedy.
2. (Medium) assert_amd64_emulation ran `docker run` via unbounded spin_cmd — a wedged
daemon or stuck pull could hang a headless install forever. Switched to
spin_cmd_bounded (TB_AMD64_SMOKE_TIMEOUT, default 120s); a 124 timeout falls through
to the same remediation, per the installer's every-docker-call-is-bounded rule.
Tests: +existing-VM colima test (no VZ flags), + bounded-smoke assertion; 11/11 in
setup-macos-arch.bats. shellcheck/style/drift clean; manifest regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread.github/workflows/release-helm-chart.yaml
Records main's ancestry without changing a byte of the tree, so the promotion
result is exactly "main becomes staging". Needed because squashed promotion
merges (647fcbb) left main with commits whose CONTENT is already in staging's
develop lineage but whose commit objects are not, so git reported a conflict
and GitHub built no merge ref - meaning the fr-gate could never run.
Verified before creating this commit: every commit on main that staging lacks is
a promotion of develop/staging content (28 merges + 1 squashed promotion), and
zero files exist only on main. Nothing is dropped. See tracebloc/backend#1416.
Comment threadscripts/manifest.sha256
Comment thread.github/workflows/chart-version-guard.yml
@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 2, 2026
@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 2, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit facdcbf. Configure here.

Comment thread.github/workflows/chart-version-guard.yml
@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 2, 2026
@tracebloc-release-train
tracebloc-release-trainBot merged commit ee54486 into mainAug 3, 2026
272 of 280 checks passed
@cursorcursorBot mentioned this pull request Aug 3, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@LukasWodka@saqlainsyed007@aptracebloc@shujaatTracebloc