-
Notifications
You must be signed in to change notification settings - Fork 1
feat(B-0849 Phase 1): Docker NixOS install.sh test harness — fast iteration (~30-60 sec) for install.sh + mise + bun + iter-5.5.0; complements B-0831 QEMU (Aaron 2026-05-27) #5393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
AceHack
merged 7 commits into
main
from
feat-b0849-1-docker-nixos-install-sh-test-harness-implementation-2026-05-27-0136z
May 27, 2026
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
484b676
feat(B-0849 Phase 1): Docker NixOS install.sh test harness — fast ite…
02cdcec
fix(PR-5393 Copilot): remove unused 'join' import — only 'resolve' is…
4f98284
fix(PR-5393 Copilot 8 findings): centralized spawnDocker helper + .do…
7365295
fix(PR-5393 Copilot 10th finding): add set -o pipefail to bun install…
76c82f5
fix(PR-5393 Copilot 4 more findings): ENV PATH for mise+bun across Do…
266f805
fix(PR-5393 lint tsc): strict typecheck fixes — coerce stdout/stderr …
5e0ddf4
fix(PR-5393 Copilot 2 more findings): docstring drift fix + digest-pi…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # .dockerignore — exclude large/irrelevant paths from docker build | ||
| # context to avoid sending gigabytes to the docker daemon per | ||
| # Copilot review on PR #5393 + per | ||
| # .claude/rules/references-upstreams-not-our-code-search-excludes.md | ||
| # (references/upstreams/ is gigabytes-of-OTHER-people's-code we | ||
| # mirror for prior-art research; NEVER ship into docker images). | ||
| # | ||
| # Build-context discipline: even though tools/ci/dockerfiles/*/Dockerfile | ||
| # only COPY's specific subtrees (tools/setup/ + .mise.toml + etc), the | ||
| # build context itself is sent to the daemon BEFORE COPY filtering | ||
| # happens. Without this .dockerignore, every docker build from repo | ||
| # root sends gigabytes of references/upstreams/ even if the Dockerfile | ||
| # only needs MB of tools/setup/. | ||
|
|
||
| # Curated prior-art / mirrors (per references-upstreams-* rule) | ||
| references/upstreams/ | ||
|
|
||
| # Node deps (npm + bun caches) | ||
| node_modules/ | ||
| .npm-cache/ | ||
|
|
||
| # Git history (large; not needed in image) | ||
| .git/ | ||
|
|
||
| # .NET build outputs | ||
| bin/ | ||
| obj/ | ||
|
|
||
| # macOS metadata | ||
| .DS_Store | ||
| **/.DS_Store | ||
|
|
||
| # Common build / cache dirs | ||
| target/ | ||
| dist/ | ||
| build/ | ||
|
|
||
| # IDE / editor scratch | ||
| .vscode/ | ||
| .idea/ | ||
| *.swp | ||
|
|
||
| # Logs that should never enter images | ||
| *.log | ||
| .docker-test-log | ||
|
|
||
| # Pre-build artifacts | ||
| *.iso | ||
| *.qcow2 | ||
| *.img | ||
| *.vmdk | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,262 @@ | ||
| #!/usr/bin/env bun | ||
| /** | ||
| * tools/ci/docker-nixos-install-sh-test.ts | ||
| * | ||
| * B-0849 Phase 1 — TS wrapper for the Docker NixOS install.sh test | ||
| * harness. Per .claude/rules/rule-0-no-sh-files.md: TS-over-bash for | ||
| * DST + cross-platform. Wraps `docker build` of | ||
| * tools/ci/dockerfiles/nixos-install-sh-test/Dockerfile with: | ||
| * | ||
| * - exit-code mapping (build success = 0; build failure = 1) | ||
| * - log capture (saved to workspace-relative path for CI artifact) | ||
| * - timeout enforcement (default 600s — install.sh + mise + bun | ||
| * + claude-code download can take a while on cold cache) | ||
| * - build-context discipline (uses repo root as context; | ||
| * dockerfile is at the fixed path; doesn't pollute root with | ||
| * build artifacts) | ||
| * | ||
| * Composes with B-0831 cascade #6 QEMU full-install test | ||
| * (qemu-full-install-test.ts): Docker = fast iteration (~30-60 sec); | ||
| * QEMU = end-to-end virtualized boot (~15 min). Both run on CI for | ||
| * install-substrate PRs. | ||
| * | ||
| * Operator framing 2026-05-27: "we should add docker based nixos | ||
| * install.sh testing so we can iterate quick that's an easy | ||
| * dockerfile" → B-0849 backlog row → this implementation. | ||
| * | ||
| * Usage: | ||
| * bun tools/ci/docker-nixos-install-sh-test.ts [--keep-image] | ||
| * | ||
| * Flags: | ||
| * --keep-image Don't `docker rmi` after the test (default: cleanup) | ||
| * | ||
| * Env: | ||
| * DOCKER_BUILD_TIMEOUT_SEC Override timeout (default 600) | ||
| * DOCKER_LOG_OUT_PATH Override log path (default | ||
| * workspace-relative .docker-test-log) | ||
|
AceHack marked this conversation as resolved.
Outdated
|
||
| * | ||
| * Exit codes: | ||
| * 0 — Docker build succeeded (install.sh + mise + bun + claude-code | ||
| * all validated on NixOS userspace) | ||
| * 1 — Docker build failed (one of the validation steps in the | ||
| * Dockerfile failed; see log) | ||
| * 2 — Usage error / missing prerequisites (docker not installed, | ||
| * wrong working directory, etc.) | ||
| * 124 — Timeout (build exceeded DOCKER_BUILD_TIMEOUT_SEC) | ||
| */ | ||
|
|
||
| import { spawnSync } from "node:child_process"; | ||
| import { existsSync, mkdirSync, writeFileSync } from "node:fs"; | ||
| import { dirname, resolve } from "node:path"; | ||
|
|
||
| // Centralized docker invocation helper — single point for the | ||
| // sonarjs/no-os-command-from-path suppression (matches the pattern | ||
| // in tools/ci/audit-installer-iso-content.ts:186-194). Rationale: | ||
| // the `docker` binary comes from the runner's default PATH; the CI | ||
| // workflow doesn't pin an absolute path, and the binary name is | ||
| // stable across docker versions. The `maxBuffer` is set generously | ||
| // because `docker build --progress=plain` produces a lot of output | ||
| // per build step (could exceed Node's default 1 MiB). | ||
| function spawnDocker( | ||
| args: string[], | ||
| opts: { timeoutMs?: number } = {} | ||
| ): ReturnType<typeof spawnSync> { | ||
| // eslint-disable-next-line sonarjs/no-os-command-from-path | ||
| return spawnSync("docker", args, { | ||
| encoding: "utf8", | ||
| maxBuffer: 64 * 1024 * 1024, // 64 MiB — docker build output is verbose | ||
| timeout: opts.timeoutMs, | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| }); | ||
| } | ||
|
|
||
|
AceHack marked this conversation as resolved.
|
||
| const DOCKERFILE_PATH = "tools/ci/dockerfiles/nixos-install-sh-test/Dockerfile"; | ||
| const IMAGE_TAG = "zeta-nixos-install-sh-test:local"; | ||
| const DEFAULT_TIMEOUT_SEC = 600; | ||
| // Default log path uses .tools/ which is .gitignored — prevents the | ||
| // log file from showing up as untracked in repo root after a local | ||
| // run (per Copilot review on PR #5393). Operator can override via | ||
| // DOCKER_LOG_OUT_PATH env var. | ||
| const DEFAULT_LOG_PATH = ".tools/docker-nixos-install-sh-test.log"; | ||
|
|
||
|
AceHack marked this conversation as resolved.
|
||
| interface BuildResult { | ||
| exitCode: 0 | 1 | 2 | 124; | ||
| reason: string; | ||
| logTail?: string; | ||
| } | ||
|
|
||
| function usage(): never { | ||
| console.error( | ||
| "usage: bun tools/ci/docker-nixos-install-sh-test.ts [--keep-image]" | ||
| ); | ||
| console.error(""); | ||
| console.error("env:"); | ||
| console.error(" DOCKER_BUILD_TIMEOUT_SEC override timeout (default 600)"); | ||
| console.error(" DOCKER_LOG_OUT_PATH override log path"); | ||
| process.exit(2); | ||
| } | ||
|
|
||
| function checkPrereqs(): void { | ||
| // Verify docker is installed (via centralized spawnDocker helper) | ||
| const docker = spawnDocker(["--version"]); | ||
| if (docker.status !== 0) { | ||
| console.error("error: docker not installed or not on PATH"); | ||
| console.error(" install via the standard mechanism for your OS"); | ||
| process.exit(2); | ||
| } | ||
|
AceHack marked this conversation as resolved.
|
||
|
|
||
| // Verify we're at repo root (Dockerfile path is repo-relative) | ||
| if (!existsSync(DOCKERFILE_PATH)) { | ||
| console.error(`error: ${DOCKERFILE_PATH} not found`); | ||
| console.error( | ||
| " run from repo root: bun tools/ci/docker-nixos-install-sh-test.ts" | ||
| ); | ||
| process.exit(2); | ||
| } | ||
|
|
||
| // Verify .mise.toml is at repo root (Dockerfile COPYs it) | ||
| if (!existsSync(".mise.toml")) { | ||
| console.error("error: .mise.toml not found at repo root"); | ||
| process.exit(2); | ||
| } | ||
| } | ||
|
|
||
| function runBuild(timeoutSec: number, logPath: string): BuildResult { | ||
| const startMs = Date.now(); | ||
| const buildArgs = [ | ||
| "build", | ||
| "--file", | ||
| DOCKERFILE_PATH, | ||
| "--tag", | ||
| IMAGE_TAG, | ||
| // --progress=plain prints full output (vs --progress=auto which | ||
| // collapses for terminals); we want full output captured to log | ||
| "--progress=plain", | ||
| // Build context = current dir (repo root) | ||
| ".", | ||
| ]; | ||
|
AceHack marked this conversation as resolved.
|
||
|
|
||
| console.log(`[B-0849 Phase 1] docker build ${buildArgs.join(" ")}`); | ||
| console.log(`[B-0849 Phase 1] timeout: ${timeoutSec}s; log: ${logPath}`); | ||
|
|
||
| // spawnDocker helper centralizes the sonarjs suppression + maxBuffer | ||
| const result = spawnDocker(buildArgs, { timeoutMs: timeoutSec * 1000 }); | ||
|
|
||
| const elapsedSec = Math.floor((Date.now() - startMs) / 1000); | ||
|
|
||
| // Capture full output to log file. spawnSync with encoding:"utf8" | ||
| // returns strings; coerce explicitly to satisfy strict typecheck | ||
| // (TS union of string|NonSharedBuffer in @types/node). | ||
| const stdout = (result.stdout ?? "").toString(); | ||
| const stderr = (result.stderr ?? "").toString(); | ||
| const fullLog = stdout + stderr; | ||
| writeFileSync(logPath, fullLog, "utf8"); | ||
|
|
||
| // Extract tail for the return-value reason | ||
| const logTail = fullLog.split("\n").slice(-20).join("\n"); | ||
|
|
||
| // result.error is Error|undefined; the .code property is Node's | ||
| // ErrnoException extension (not on base Error). Type-assert as | ||
| // NodeJS.ErrnoException to access .code without TS complaint. | ||
| const errCode = (result.error as NodeJS.ErrnoException | undefined)?.code; | ||
| if (result.signal === "SIGTERM" || errCode === "ETIMEDOUT") { | ||
| return { | ||
| exitCode: 124, | ||
| reason: `docker build timed out after ${timeoutSec}s (actual: ${elapsedSec}s)`, | ||
| logTail, | ||
| }; | ||
| } | ||
|
|
||
| if (result.status === 0) { | ||
| console.log( | ||
| `[B-0849 Phase 1] SUCCESS — docker build completed in ${elapsedSec}s` | ||
| ); | ||
| return { | ||
| exitCode: 0, | ||
| reason: `docker build succeeded in ${elapsedSec}s`, | ||
| logTail, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| exitCode: 1, | ||
| reason: `docker build failed (exit ${result.status}) after ${elapsedSec}s`, | ||
| logTail, | ||
| }; | ||
| } | ||
|
|
||
| function cleanup(keepImage: boolean): void { | ||
| if (keepImage) { | ||
| console.log( | ||
| `[B-0849 Phase 1] --keep-image set; image ${IMAGE_TAG} retained for inspection` | ||
| ); | ||
| return; | ||
| } | ||
| // spawnDocker helper centralizes the sonarjs suppression | ||
| const rm = spawnDocker(["rmi", "-f", IMAGE_TAG]); | ||
| if (rm.status === 0) { | ||
| console.log(`[B-0849 Phase 1] cleaned up image ${IMAGE_TAG}`); | ||
| } else { | ||
| console.error( | ||
| `[B-0849 Phase 1] warning: docker rmi ${IMAGE_TAG} failed (non-fatal)` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function main(): void { | ||
| // Parse args | ||
| const args = process.argv.slice(2); | ||
| let keepImage = false; | ||
| for (const arg of args) { | ||
| if (arg === "--keep-image") { | ||
| keepImage = true; | ||
| } else if (arg === "--help" || arg === "-h") { | ||
| usage(); | ||
| } else { | ||
| console.error(`error: unknown arg: ${arg}`); | ||
| usage(); | ||
| } | ||
| } | ||
|
|
||
| // Resolve env overrides | ||
| const timeoutSec = parseInt( | ||
| process.env.DOCKER_BUILD_TIMEOUT_SEC ?? String(DEFAULT_TIMEOUT_SEC), | ||
| 10 | ||
| ); | ||
| if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) { | ||
| console.error( | ||
| `error: DOCKER_BUILD_TIMEOUT_SEC must be a positive integer (got: ${process.env.DOCKER_BUILD_TIMEOUT_SEC})` | ||
| ); | ||
| process.exit(2); | ||
| } | ||
| const logPath = resolve(process.env.DOCKER_LOG_OUT_PATH ?? DEFAULT_LOG_PATH); | ||
|
|
||
| // Ensure log directory exists (Copilot review: use path.dirname | ||
| // instead of lastIndexOf("/") for cross-platform support including | ||
| // Windows backslash paths). | ||
| const logDir = dirname(logPath); | ||
| if (logDir && !existsSync(logDir)) { | ||
| mkdirSync(logDir, { recursive: true }); | ||
| } | ||
|
AceHack marked this conversation as resolved.
|
||
|
|
||
| checkPrereqs(); | ||
|
|
||
| const result = runBuild(timeoutSec, logPath); | ||
|
|
||
| console.log(""); | ||
| console.log(`[B-0849 Phase 1] result: ${result.reason}`); | ||
| console.log(`[B-0849 Phase 1] log: ${logPath}`); | ||
|
|
||
| if (result.exitCode !== 0) { | ||
| console.log("[B-0849 Phase 1] tail of build log:"); | ||
| console.log("--- BEGIN TAIL ---"); | ||
| console.log(result.logTail); | ||
| console.log("--- END TAIL ---"); | ||
| } | ||
|
|
||
| cleanup(keepImage); | ||
|
|
||
| process.exit(result.exitCode); | ||
| } | ||
|
|
||
| main(); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.