Skip to content

release-train: develop -> staging - #499

Merged
tracebloc-release-train[bot] merged 12 commits into
stagingfrom
release-train/to-staging
Jul 30, 2026
Merged

release-train: develop -> staging#499
tracebloc-release-train[bot] merged 12 commits into
stagingfrom
release-train/to-staging

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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


Note

Medium Risk
Large, customer-facing installer and CI gate changes (hard-fail code quality, Helm index backstop); well-tested but any regression affects fresh installs on Windows and Linux/WSL.

Overview
Automated develop → staging promotion with client chart 1.9.8 (Chart.yaml version/appVersion).

CI / release: Code quality now fails on findings (soft-fail: false) and supports workflow_dispatch full-repo scans. Post-publish Helm index verification reads index.yaml via the GitHub Contents API (avoids stale raw.githubusercontent.com) and treats an empty read as failure; workflow errors go to stdout for Actions annotations.

Windows installer (install-k8s.ps1): Flow is 6 steps (preflight vs “Installing system tools”). Adds UAC self-elevation for non-admin runs, Invoke-WithHeartbeat / tool summary lines, killable Start-Process + deadlines for winget/Docker/k3d, silent Docker Desktop (wsl-2, --always-run-service), smarter WSL update (--web-download, version floor), daily-user Docker provisioning (docker-users, .wslconfig), and memory preflight that labels host RAM but grades Docker’s budget (Show-MemoryStatus).

Linux installer:probe.sh detects WSL2 and updates Tier 2 / audit copy (WSL2 rootless preferred). setup-linux.sh gates rootless on per-user systemd, routes failures through _tier2_fallthroughprepare-host with TB_PREPARE_USER, and tightens messaging on privileged prerequisites.

Docs:INSTALL.md expands Windows paths (cmd one-liner, WSL2 rootless, admin / relaunch behavior). scripts/manifest.sha256 and large Pester/bats coverage for the new behavior.

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

shujaatTraceblocand others added 10 commits July 30, 2026 08:33
…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).
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

…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>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

👋 Heads-up — Code review queue is at 40 / 30

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

Comment threadscripts/install-k8s.ps1
Comment threadscripts/install-k8s.ps1
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Both findings valid, both ticketed as #500, neither a promotion blocker.

  1. Stale five-step roadmapPrint-Roadmap wasn't updated when the Installing system tools phase was added, so operators see Step 2/6 downloading ~700 MB while the printed roadmap still says step 2 is cluster setup, and every later step is misnumbered. That directly undercuts the honest-progress split the change was making.
  2. Missing Start-Process output redirects — the new Docker Desktop (winget + direct) and winget k3d/helm installs don't capture stdout/stderr, so a failure leaves only a generic exit code in the log and the -Diagnose bundle. The same PR already does this correctly for the WSL and k3d cluster start paths, so it's an inconsistency within one change. Given Windows installs are the ones we debug remotely from a customer's log, losing installer stderr turns a one-look diagnosis into a round trip.

Both are in the Windows installer epic's area (backend#1285) and flagged to @saadqbal on #500.

For the record on why this PR didn't merge: it was not these findings. The staging fr-gate failed with #485 not on the kanban board — Arturo's #485 (merged 10:49) had never been added to project #2 at all, so the gate correctly failed closed on an item it couldn't verify. I've added the card at On dev; add-to-kanban missing a PR is the membership gap kanban-reconcile.yml sweeps on a schedule, which wouldn't have run in time for this hop.

Resolving these so the re-fire isn't held.

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

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

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

Reviewed by Cursor Bugbot for commit 80bee57. Configure here.

@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@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 Jul 30, 2026

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

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

Reviewed by Cursor Bugbot for commit 80bee57. Configure here.

@tracebloc-release-train
tracebloc-release-trainBot merged commit 3c24562 into stagingJul 30, 2026
42 of 43 checks passed
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.

3 participants

@LukasWodka@shujaatTracebloc@aptracebloc