From 83a5a6cbec10075e444d4850699a4c562b2f36bf Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 2 Apr 2026 11:44:43 -0700 Subject: [PATCH] fix: unblock oven nix iso upload --- fedac/native/scripts/upload-release.sh | 19 ++- oven/deploy.sh | 4 + oven/native-builder.mjs | 221 +++++++++++++------------ 3 files changed, 133 insertions(+), 111 deletions(-) diff --git a/fedac/native/scripts/upload-release.sh b/fedac/native/scripts/upload-release.sh index 77f0d1988c..a32eca9f7b 100755 --- a/fedac/native/scripts/upload-release.sh +++ b/fedac/native/scripts/upload-release.sh @@ -60,20 +60,27 @@ DO_SPACES_REGION="${DO_SPACES_REGION:-sfo3}" BASE_URL="https://${DO_SPACES_BUCKET}.${DO_SPACES_REGION}.digitaloceanspaces.com" # Build version string from git. Dirty/conflicted uploads are blocked by default. -GIT_HASH=$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown") -CONFLICT_FILES=$(git -C "$SCRIPT_DIR" diff --name-only --diff-filter=U 2>/dev/null || true) +GIT_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || true) +GIT_CWD="$SCRIPT_DIR" +GIT_NATIVE_PATHS=() +if [ -n "$GIT_ROOT" ]; then + GIT_CWD="$GIT_ROOT" + GIT_NATIVE_PATHS=(fedac/native fedac/nixos) +fi +GIT_HASH=$(git -C "$GIT_CWD" rev-parse --short HEAD 2>/dev/null || echo "unknown") +CONFLICT_FILES=$(git -C "$GIT_CWD" diff --name-only --diff-filter=U -- "${GIT_NATIVE_PATHS[@]}" 2>/dev/null || true) if [ -n "$CONFLICT_FILES" ]; then echo "Error: refusing upload with unresolved merge conflicts:" >&2 echo "$CONFLICT_FILES" >&2 exit 1 fi DIRTY_TRACKED=0 -if ! git -C "$SCRIPT_DIR" diff --quiet HEAD 2>/dev/null; then +if ! git -C "$GIT_CWD" diff --quiet HEAD -- "${GIT_NATIVE_PATHS[@]}" 2>/dev/null; then DIRTY_TRACKED=1 fi if [ "$DIRTY_TRACKED" -eq 1 ] && [ "${ALLOW_DIRTY_UPLOAD:-0}" != "1" ]; then - echo "Error: refusing dirty upload. Commit/stash/reset native changes first." >&2 - git -C "$SCRIPT_DIR" status --porcelain --untracked-files=no -- fedac/native 2>/dev/null >&2 || true + echo "Error: refusing dirty upload. Commit/stash/reset fedac/native or fedac/nixos changes first." >&2 + git -C "$GIT_CWD" status --porcelain --untracked-files=no -- "${GIT_NATIVE_PATHS[@]}" 2>/dev/null >&2 || true echo "Override only for emergencies: ALLOW_DIRTY_UPLOAD=1 ./scripts/upload-release.sh ..." >&2 exit 1 fi @@ -180,7 +187,7 @@ curl -sf "${BASE_URL}/os/${CHANNEL_PREFIX}releases.json" -o "$RELEASES_JSON" 2>/ || echo '{"releases":[]}' > "$RELEASES_JSON" # Append new entry (keep last 50) -COMMIT_MSG=$(git -C "$SCRIPT_DIR" log -1 --format="%s" 2>/dev/null || echo "") +COMMIT_MSG=$(git -C "$GIT_CWD" log -1 --format="%s" 2>/dev/null || echo "") BUILD_HANDLE="${AC_HANDLE:-}" python3 - "$RELEASES_JSON" "$FULL_VERSION" "$SHA256" "$SIZE" "$GIT_HASH" "$BUILD_TS" "$BUILD_NAME" "$CHANNEL_PREFIX" "$COMMIT_MSG" "$BUILD_HANDLE" <<'PYEOF' diff --git a/oven/deploy.sh b/oven/deploy.sh index e7ab5820d4..11de570be9 100755 --- a/oven/deploy.sh +++ b/oven/deploy.sh @@ -116,6 +116,10 @@ ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "root@$OVEN_HOST" bash -s <<'NIX_E else echo "WARNING: nix binary not found after install" fi + if id -u oven >/dev/null 2>&1; then + mkdir -p /home/oven/.cache/nix + chown -R oven:oven /home/oven/.cache + fi # Enable flakes mkdir -p /etc/nix grep -q 'experimental-features' /etc/nix/nix.conf 2>/dev/null || \ diff --git a/oven/native-builder.mjs b/oven/native-builder.mjs index 6276d36ebd..5c37275d2c 100644 --- a/oven/native-builder.mjs +++ b/oven/native-builder.mjs @@ -331,7 +331,14 @@ async function runBuildJob(job) { const repoDir = path.resolve(NATIVE_DIR, "../.."); - // Preflight: hard-sync build repo and refuse conflicted/dirty native trees. + // Determine variant: "c" (default), "cl", "nix", "both", or "all" + const variant = job.variant || "c"; + const buildC = variant === "c" || variant === "both" || variant === "all"; + const buildCL = variant === "cl" || variant === "both" || variant === "all"; + const buildNix = variant === "nix" || variant === "all"; + const needsDockerBuild = buildC || buildCL; + + // Preflight: hard-sync build repo and refuse conflicted/dirty native/Nix trees. addLogLine(job, "stdout", "Preflight: syncing native git checkout..."); await runPhase(job, "preflight-sync", "bash", ["-lc", [ "set -euo pipefail", @@ -340,7 +347,7 @@ async function runBuildJob(job) { `if git rev-parse --verify origin/${NATIVE_BRANCH} >/dev/null 2>&1; then`, ` git reset --hard origin/${NATIVE_BRANCH} --quiet`, "fi", - "git clean -fdq -- fedac/native", + "git clean -fdq -- fedac/native fedac/nixos", ].join("\n")], repoDir); const syncedRef = await runSync("git", ["rev-parse", "HEAD"], repoDir); @@ -348,18 +355,18 @@ async function runBuildJob(job) { const trackedDirty = await runSync( "git", - ["status", "--porcelain", "--untracked-files=no", "--", "fedac/native"], + ["status", "--porcelain", "--untracked-files=no", "--", "fedac/native", "fedac/nixos"], repoDir, ); if (trackedDirty) { throw new Error( - `Refusing native build: fedac/native tree is dirty after sync:\n${trackedDirty}`, + `Refusing native build: fedac/native or fedac/nixos tree is dirty after sync:\n${trackedDirty}`, ); } const unresolved = await runSync( "git", - ["diff", "--name-only", "--diff-filter=U", "--", "fedac/native"], + ["diff", "--name-only", "--diff-filter=U", "--", "fedac/native", "fedac/nixos"], repoDir, ); if (unresolved) { @@ -412,40 +419,36 @@ async function runBuildJob(job) { job.commitMsg = commitMsg; const vmlinuzOut = `/tmp/oven-vmlinuz-${job.id}`; - // Pre-build: aggressive prune to avoid disk-full failures (60GB droplet fills fast) - addLogLine(job, "stdout", "Pre-build: Pruning Docker artifacts..."); - try { - await runPhase(job, "prune", "bash", ["-c", - "docker container prune -f && docker image prune -af --filter until=2h && docker builder prune -af --filter until=30m && docker volume prune -f", - ], repoDir); - // Check free space — abort early if less than 10GB free - const dfOut = await runSync("bash", ["-c", "df --output=avail / | tail -1"], repoDir); - const availKB = parseInt(dfOut, 10) || 0; - const availGB = availKB / 1048576; - addLogLine(job, "stdout", ` Disk: ${availGB.toFixed(1)}GB free`); - if (availGB < 10) { - addLogLine(job, "stderr", ` WARNING: Only ${availGB.toFixed(1)}GB free — running full prune...`); - await runPhase(job, "emergency-prune", "bash", ["-c", - "docker system prune -af --volumes", + // Pre-build: prune Docker only when a Docker-backed variant is needed. + if (needsDockerBuild) { + addLogLine(job, "stdout", "Pre-build: Pruning Docker artifacts..."); + try { + await runPhase(job, "prune", "bash", ["-c", + "docker container prune -f && docker image prune -af --filter until=2h && docker builder prune -af --filter until=30m && docker volume prune -f", ], repoDir); - } - } catch { addLogLine(job, "stdout", " Prune skipped (non-fatal)"); } - - // Phase 1: Docker image build (cached layers = fast) - addLogLine(job, "stdout", "Phase 1: Building Docker image..."); - await runPhase(job, "docker-build", "docker", [ - "build", "-t", "ac-os-builder", - "-f", path.join(repoDir, "fedac/native/Dockerfile.builder"), - repoDir, - ], repoDir); + const dfOut = await runSync("bash", ["-c", "df --output=avail / | tail -1"], repoDir); + const availKB = parseInt(dfOut, 10) || 0; + const availGB = availKB / 1048576; + addLogLine(job, "stdout", ` Disk: ${availGB.toFixed(1)}GB free`); + if (availGB < 10) { + addLogLine(job, "stderr", ` WARNING: Only ${availGB.toFixed(1)}GB free — running full prune...`); + await runPhase(job, "emergency-prune", "bash", ["-c", + "docker system prune -af --volumes", + ], repoDir); + } + } catch { addLogLine(job, "stdout", " Prune skipped (non-fatal)"); } + + // Phase 1: Docker image build (cached layers = fast) + addLogLine(job, "stdout", "Phase 1: Building Docker image..."); + await runPhase(job, "docker-build", "docker", [ + "build", "-t", "ac-os-builder", + "-f", path.join(repoDir, "fedac/native/Dockerfile.builder"), + repoDir, + ], repoDir); - job.percent = 30; + job.percent = 30; + } - // Determine variant: "c" (default), "cl", "nix", "both", or "all" - const variant = job.variant || "c"; - const buildC = variant === "c" || variant === "both" || variant === "all"; - const buildCL = variant === "cl" || variant === "both" || variant === "all"; - const buildNix = variant === "nix" || variant === "all"; const uploadScript = path.join(NATIVE_DIR, "scripts/upload-release.sh"); const uploadEnv = { DO_SPACES_KEY: process.env.DO_SPACES_KEY || process.env.ART_SPACES_KEY || "", @@ -545,6 +548,7 @@ async function runBuildJob(job) { // ── NixOS variant: build directly on host with nix (no Docker) ── if (buildNix) { const nixosDir = path.resolve(NATIVE_DIR, "../nixos"); + const nixHomeDir = `/tmp/oven-nix-home-${job.id}`; const nixUploadDir = `/tmp/oven-nix-upload-${job.id}`; const nixBin = await resolveBinary("nix", NIX_BIN_CANDIDATES, nixosDir); if (!nixBin) { @@ -561,8 +565,11 @@ async function runBuildJob(job) { ], nixosDir, ); + await fs.mkdir(path.join(nixHomeDir, ".cache", "nix"), { recursive: true }); const nixEnv = { - NIX_CONFIG: "experimental-features = nix-command flakes", + HOME: nixHomeDir, + XDG_CACHE_HOME: path.join(nixHomeDir, ".cache"), + NIX_CONFIG: "experimental-features = nix-command flakes\nwarn-dirty = false", AC_NIX_NATIVE_SRC: NATIVE_DIR, PATH: uniqueNonEmpty([ path.dirname(nixBin), @@ -572,80 +579,84 @@ async function runBuildJob(job) { }; addLogLine(job, "stdout", `Phase N: using nix at ${nixBin}`); - // Preflight: garbage collect old nix store entries - addLogLine(job, "stdout", "Phase N: NixOS — cleaning Nix store..."); - if (nixGcBin) { - try { - await runPhase( - job, - "nix-gc", - nixGcBin, - ["--delete-older-than", "3d"], - nixosDir, - nixEnv, - ); - } catch {} - } else { - addLogLine(job, "stdout", "Phase N: skipping Nix GC — nix-collect-garbage not found"); - } - - addLogLine(job, "stdout", "Phase N: NixOS — building image with Nix..."); - job.stage = "nix-build"; - job.percent = Math.max(job.percent, 60); - if (progressCallback) progressCallback(makeSnapshot(job)); - - // fedac/nixos reads AC_NIX_NATIVE_SRC from the host env to import fedac/native. - // Build the NixOS ISO image - await runPhase(job, "nix-build", nixBin, [ - "build", ".#usb-image", - "--impure", - "--no-link", "--print-out-paths", - ], nixosDir, nixEnv); - - job.percent = Math.max(job.percent, 85); - - // Extract ISO path from nix build output - const nixOutResult = await runSync( - nixBin, - ["build", ".#usb-image", "--impure", "--no-link", "--print-out-paths"], - nixosDir, - nixEnv, - ); - if (!nixOutResult) { - throw new Error("NixOS build finished without returning an output path"); - } - - // Find the ISO in the output directory - const isoPath = await runSync( - "bash", - ["-lc", "find \"$1\" -name '*.iso' -type f | head -1", "_", nixOutResult], - nixosDir, - ); + try { + // Preflight: garbage collect old nix store entries + addLogLine(job, "stdout", "Phase N: NixOS — cleaning Nix store..."); + if (nixGcBin) { + try { + await runPhase( + job, + "nix-gc", + nixGcBin, + ["--delete-older-than", "3d"], + nixosDir, + nixEnv, + ); + } catch {} + } else { + addLogLine(job, "stdout", "Phase N: skipping Nix GC — nix-collect-garbage not found"); + } + + addLogLine(job, "stdout", "Phase N: NixOS — building image with Nix..."); + job.stage = "nix-build"; + job.percent = Math.max(job.percent, 60); + if (progressCallback) progressCallback(makeSnapshot(job)); + + // fedac/nixos reads AC_NIX_NATIVE_SRC from the host env to import fedac/native. + // Build the NixOS ISO image + await runPhase(job, "nix-build", nixBin, [ + "build", ".#usb-image", + "--impure", + "--no-link", "--print-out-paths", + ], nixosDir, nixEnv); + + job.percent = Math.max(job.percent, 85); + + // Extract ISO path from nix build output + const nixOutResult = await runSync( + nixBin, + ["build", ".#usb-image", "--impure", "--no-link", "--print-out-paths"], + nixosDir, + nixEnv, + ); + if (!nixOutResult) { + throw new Error("NixOS build finished without returning an output path"); + } + + // Find the ISO in the output directory + const isoPath = await runSync( + "bash", + ["-lc", "find \"$1\" -name '*.iso' -type f | head -1", "_", nixOutResult], + nixosDir, + ); - if (!isoPath) { - throw new Error("NixOS build produced no ISO file"); - } + if (!isoPath) { + throw new Error("NixOS build produced no ISO file"); + } - addLogLine(job, "stdout", `NixOS image: ${isoPath}`); + addLogLine(job, "stdout", `NixOS image: ${isoPath}`); - // Copy to upload directory - await fs.mkdir(nixUploadDir, { recursive: true }); - const nixIsoUpload = path.join(nixUploadDir, "ac-os-nixos.iso"); - await fs.copyFile(isoPath, nixIsoUpload); + // Copy to upload directory + await fs.mkdir(nixUploadDir, { recursive: true }); + const nixIsoUpload = path.join(nixUploadDir, "ac-os-nixos.iso"); + await fs.copyFile(isoPath, nixIsoUpload); - job.stage = "nix-upload"; - job.percent = Math.max(job.percent, 90); + job.stage = "nix-upload"; + job.percent = Math.max(job.percent, 90); - // Upload with nix- channel prefix - await runPhase(job, "nix-upload", "bash", [ - uploadScript, "--iso", nixIsoUpload, - ], NATIVE_DIR, { - ...uploadEnv, - OTA_CHANNEL: "nix", - }); + // Upload with nix- channel prefix + await runPhase(job, "nix-upload", "bash", [ + uploadScript, "--iso", nixIsoUpload, + ], NATIVE_DIR, { + ...uploadEnv, + OTA_CHANNEL: "nix", + }); - try { await fs.rm(nixUploadDir, { recursive: true }); } catch {} - addLogLine(job, "stdout", "NixOS variant uploaded successfully"); + addLogLine(job, "stdout", "NixOS variant uploaded successfully"); + } finally { + try { await fs.rm(nixUploadDir, { recursive: true }); } catch {} + try { await fs.rm(nixHomeDir, { recursive: true }); } catch {} + } } job.status = "success"; -- 2.51.2