From ded2bfbea3983a1601cef25d9ddb854cfde29622 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 6 Jul 2026 14:12:31 +0000 Subject: [PATCH] ci: share ISO build steps between test and release workflows Extract the duplicated Nix-setup and image-build steps into two composite actions so the release workflow builds images the same way the test workflow does: - .github/actions/setup-nix: install Nix natively + cache /nix/store, with configurable cache prefix / gc cap (nix-images default, nix-flake/5G for the cheap flake-check job). - .github/actions/build-iso: build one image kind (full ISO or drv-only), dereferencing the image + .sha256 sidecar into a dist dir. release.yml and test.yml (flake + Images jobs) now use these shared actions. Release keeps the slow default compression and plain version names; test keeps its fast compression and PR/branch stamping via the action inputs. --- .github/actions/build-iso/action.yml | 95 ++++++++++++++++++++ .github/actions/setup-nix/action.yml | 43 +++++++++ .github/workflows/release.yml | 35 +++----- .github/workflows/test.yml | 128 ++++++++++++--------------- 4 files changed, 205 insertions(+), 96 deletions(-) create mode 100644 .github/actions/build-iso/action.yml create mode 100644 .github/actions/setup-nix/action.yml diff --git a/.github/actions/build-iso/action.yml b/.github/actions/build-iso/action.yml new file mode 100644 index 0000000..f6b0e3c --- /dev/null +++ b/.github/actions/build-iso/action.yml @@ -0,0 +1,95 @@ +# Build one coder/box image kind for the runner's native architecture. +# +# Shared by the test and release workflows so both build images the exact same +# way. Nix is already on the host (see the setup-nix action), so make/git +# (preinstalled on the runner) build straight into the host /nix/store — no +# container. A full build runs `make /iso`, then dereferences the image +# and its .sha256 sidecar (colocated in out/-iso/iso) into a real dir so +# a single upload carries both; a drv-only build just instantiates the +# derivation (`make /drv`, cheap validation, no image). Bare target → +# native currentSystem (callers pin the matching native runner). +name: Build coder/box ISO +description: >- + Build a coder/box image kind (installer or appliance) as a full ISO or, for + cheap validation, just instantiate its derivation. + +inputs: + target: + description: Image kind to build — "installer" or "appliance". + required: true + full: + description: >- + "true" builds the full ISO (make /iso) and dereferences the image + + its .sha256 sidecar into the output dir; "false" only instantiates the + derivation (make /drv — cheap validation, no image). + required: false + default: "true" + iso-compression: + description: >- + Optional squashfs compression override passed as ISO_COMPRESSION (e.g. + "zstd -Xcompression-level 3" to trade ISO size for build speed on + verification builds). Empty keeps the nixpkgs default so shipped ISOs stay + small. + required: false + default: "" + pr-title: + description: >- + PR title woven into the pretty version name (coderBox.prTitle). Empty for + tag/main builds so they keep their plain names. + required: false + default: "" + pr-number: + description: PR number woven into the pretty version name (coderBox.prNumber). + required: false + default: "" + branch: + description: >- + Source branch for the boot-screen "@" stamp + (github.head_ref). Empty falls back to the Makefile's local branch name. + required: false + default: "" + dist: + description: >- + Directory to collect built ISO(s) + .sha256 sidecars into. Empty creates a + fresh mktemp dir; pass an existing dir to accumulate multiple kinds into + one place (the test workflow builds installer + appliance side by side). + required: false + default: "" + +outputs: + dist: + description: >- + Absolute path to the dir holding the built ISO(s) + .sha256 sidecars (no + ISO added for a drv-only build). + value: ${{ steps.build.outputs.dist }} + +runs: + using: composite + steps: + - name: Build image + id: build + shell: bash + env: + TARGET: ${{ inputs.target }} + FULL: ${{ inputs.full }} + DIST: ${{ inputs.dist }} + # Read by the Makefile under --impure for the image's pretty version + # name / boot-screen label. Set through env (not inlined into the script) + # so an arbitrary PR title can't break the shell; empty on non-PR events. + ISO_COMPRESSION: ${{ inputs.iso-compression }} + CODER_BOX_PR_TITLE: ${{ inputs.pr-title }} + CODER_BOX_PR_NUMBER: ${{ inputs.pr-number }} + CODER_BOX_BRANCH: ${{ inputs.branch }} + run: | + # Reuse the caller-provided dist dir or make a fresh one, then expose it + # so later workflow steps (size recording, artifact upload) can find the + # built image(s). + dist="${DIST:-$(mktemp -d)}" + echo "dist=$dist" >>"$GITHUB_OUTPUT" + if [ "$FULL" = "true" ]; then + make "$TARGET/iso" + cp -L "out/$TARGET-iso/iso"/* "$dist/" + else + make "$TARGET/drv" + fi + ls -lh "$dist" diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml new file mode 100644 index 0000000..f62076a --- /dev/null +++ b/.github/actions/setup-nix/action.yml @@ -0,0 +1,43 @@ +# Install Nix natively on the runner and cache the /nix/store across runs. +# +# Shared by the test and release workflows so every job sets Nix up the exact +# same way. Nix is installed on the host (not the `nixos/nix` container — that +# container lacks a standard glibc loader, so GitHub's bundled Node couldn't run +# there and JS actions failed); installing on the host avoids that and, crucially, +# lets us cache the /nix/store (nix-community/cache-nix-action, backed by the +# GitHub Actions cache). Each arch runs on its own native runner, so bare +# `make ` resolves to the runner's native `builtins.currentSystem`. +name: Set up Nix +description: >- + Install Nix natively on the runner and cache the /nix/store so bare + `make ` builds straight into the host store. + +inputs: + cache-key-prefix: + description: >- + Prefix for the /nix/store cache key (e.g. "nix-images" for image builds, + "nix-flake" for the cheap flake-check job) so unrelated jobs don't share a + cache entry. + required: false + default: nix-images + gc-max-store-size: + description: >- + Cap on the saved /nix/store size (cache-nix-action gc-max-store-size-linux) + so a run can't blow past the repo's GitHub Actions cache budget (10G total). + required: false + default: 8G + +runs: + using: composite + steps: + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Cache Nix store + uses: nix-community/cache-nix-action@v6 + with: + # Key on the lockfile + all Nix sources; restore the most recent + # arch-matching cache otherwise. + primary-key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock', '**/*.nix') }} + restore-prefixes-first-match: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-${{ runner.arch }}- + gc-max-store-size-linux: ${{ inputs.gc-max-store-size }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82ee193..76fcb8a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,34 +80,19 @@ jobs: # to release an arbitrary commit. ref: ${{ github.event.inputs.ref }} - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - - - name: Cache Nix store - uses: nix-community/cache-nix-action@v6 - with: - # ISO closures are large; cap the saved store so a build can't blow - # past the repo's GitHub Actions cache budget (10G total). - primary-key: nix-images-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock', '**/*.nix') }} - restore-prefixes-first-match: nix-images-${{ runner.os }}-${{ runner.arch }}- - gc-max-store-size-linux: 8G + # Install Nix natively + cache the /nix/store (shared with the test + # workflow so releases build the exact same way). + - name: Set up Nix + uses: ./.github/actions/setup-nix + # Build the full ISO via the shared action. No ISO_COMPRESSION / PR + # stamping inputs: releases keep the slow nixpkgs default (small shipped + # ISOs) and their plain version names. - name: Build ISO id: build - env: - TARGET: ${{ matrix.target }} - run: | - # Nix is on the host, so make/git (preinstalled on the runner) build - # straight into the host /nix/store. The ISO target puts the image and - # its .sha256 sidecar together in out/-iso/iso, so a single - # cp -L dereferences both into a real mktemp dir for upload (release - # consumers can verify the download). Bare target → native - # currentSystem (matrix pins the matching native runner). - dist="$(mktemp -d)" - echo "dist=$dist" >>"$GITHUB_OUTPUT" - make "$TARGET/iso" - cp -L "out/$TARGET-iso/iso"/* "$dist/" - ls -lh "$dist" + uses: ./.github/actions/build-iso + with: + target: ${{ matrix.target }} - name: Upload ISO artifact uses: actions/upload-artifact@v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3835ee7..1f3705c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,18 +79,14 @@ jobs: with: ref: ${{ github.event.inputs.ref }} - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - - - name: Cache Nix store - uses: nix-community/cache-nix-action@v6 + # Install Nix natively + cache the /nix/store. The flake check builds + # nothing, so it gets its own smaller cache (distinct prefix + gc cap) so + # it doesn't share an entry with the large image builds. + - name: Set up Nix + uses: ./.github/actions/setup-nix with: - # Key on the lockfile + all Nix sources; restore the most recent - # arch-matching cache otherwise. Cap the saved store so a run can't - # blow past the repo's GitHub Actions cache budget. - primary-key: nix-flake-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock', '**/*.nix') }} - restore-prefixes-first-match: nix-flake-${{ runner.os }}-${{ runner.arch }}- - gc-max-store-size-linux: 5G + cache-key-prefix: nix-flake + gc-max-store-size: 5G - name: Flake check run: make check @@ -146,22 +142,19 @@ jobs: name: Build ${{ needs.plan.outputs.kinds }} (${{ matrix.system }}) runs-on: ${{ matrix.runner }} env: - # Resolved per-kind plan from the `plan` job. Steps below branch on these. + # Resolved per-kind plan from the `plan` job. Steps below (the build-plan + # summary + the per-kind build/upload gating) branch on these. The + # build-iso action instead receives the plan per kind via its `full` input. + # + # PR title/number + branch are passed straight to the build-iso action's + # inputs (pr-title / pr-number / branch): the title + number are woven into + # the image's pretty version name (coderBox.prTitle / coderBox.prNumber), + # and the branch feeds the boot-screen "@" stamp + # (github.head_ref is the real source branch on PRs — a PR checkout is a + # detached HEAD — and empty otherwise so the Makefile falls back to the + # local branch name; tag/main builds keep their plain names). INSTALLER_FULL: ${{ needs.plan.outputs.installer_full }} APPLIANCE_FULL: ${{ needs.plan.outputs.appliance_full }} - # PR title + number woven into the image's pretty version name (boot-menu - # label + ISO file name) via coderBox.prTitle / coderBox.prNumber. Set - # through `env:` (not inlined into a run script) so an arbitrary title - # can't break the shell, and empty for non-PR events so tag/main builds - # keep their plain names. - CODER_BOX_PR_TITLE: ${{ github.event.pull_request.title }} - CODER_BOX_PR_NUMBER: ${{ github.event.pull_request.number }} - # Branch name for the boot-screen label's "@" stamp. A - # PR checkout is a detached HEAD (so the Makefile's `git rev-parse - # --abbrev-ref HEAD` would say "HEAD"); github.head_ref is the real source - # branch on pull_request events and empty otherwise (the Makefile then - # falls back to the local branch name). - CODER_BOX_BRANCH: ${{ github.head_ref }} strategy: fail-fast: false matrix: @@ -178,62 +171,55 @@ jobs: # dispatch to build an arbitrary commit. ref: ${{ github.event.inputs.ref }} - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@main - - - name: Cache Nix store - uses: nix-community/cache-nix-action@v6 - with: - # ISO closures are large; cap the saved store so a build can't blow - # past the repo's GitHub Actions cache budget (10G total). - primary-key: nix-images-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock', '**/*.nix') }} - restore-prefixes-first-match: nix-images-${{ runner.os }}-${{ runner.arch }}- - gc-max-store-size-linux: 8G + # Install Nix natively + cache the /nix/store (shared with the release + # workflow so both build images the exact same way). + - name: Set up Nix + uses: ./.github/actions/setup-nix - # Per-kind plan (full ISO vs drv only) is in the job name; the run summary - # below also records it. INSTALLER_FULL / APPLIANCE_FULL come from the - # job-level env above. - - name: Build images + # Record the per-kind plan (full ISO vs drv only, in the job name too) in + # the run summary, and stage one dist dir that both kinds' builds collect + # into so the size/upload steps below have a single place to look. + # INSTALLER_FULL / APPLIANCE_FULL come from the job-level env above. + - name: Build plan id: build - env: - # These are verification images, not shipped artifacts, so trade ISO - # size for build speed: a low squashfs compression level is far faster - # than the nixpkgs default (zstd level 19), which otherwise dominates - # the build — and the rev baked into /etc forces that recompress on - # every commit regardless of caching. Releases keep the slow default. - ISO_COMPRESSION: zstd -Xcompression-level 3 run: | - # Record the per-kind plan in the run summary for quick scanning. plan() { [ "$1" = "true" ] && echo "full ISO" || echo "derivation only"; } { echo "### Build plan (${{ matrix.system }})" echo "- installer: $(plan "$INSTALLER_FULL")" echo "- appliance: $(plan "$APPLIANCE_FULL")" } >>"$GITHUB_STEP_SUMMARY" + echo "dist=$(mktemp -d)" >>"$GITHUB_OUTPUT" - # Nix is on the host now, so make/git (preinstalled on the runner) - # build straight into the host /nix/store — no container. A full build - # → make /iso, then dereference the image + its .sha256 sidecar - # (colocated in out/-iso/iso) into a real dir for upload; a - # drv-only kind just instantiates. Bare target → native currentSystem. - dist="$(mktemp -d)" - echo "dist=$dist" >>"$GITHUB_OUTPUT" - # Nix is on the host (make/git preinstalled on the runner) so the - # build goes straight into the host /nix/store — no container. The - # job-level CODER_BOX_PR_TITLE / CODER_BOX_PR_NUMBER env is inherited - # by make directly (read under --impure for the pretty version name). - build_kind() { - kind="$1"; full="$2" - if [ "$full" = "true" ]; then - make "$kind/iso" - cp -L "out/$kind-iso/iso"/* "$dist/" - else - make "$kind/drv" - fi - } - build_kind installer "$INSTALLER_FULL" - build_kind appliance "$APPLIANCE_FULL" - ls -lh "$dist" + # Build each kind through the shared action (same as the release workflow). + # These are verification images, not shipped artifacts, so trade ISO size + # for build speed via ISO_COMPRESSION: a low squashfs compression level is + # far faster than the nixpkgs default (zstd level 19), which otherwise + # dominates the build — and the rev baked into /etc forces that recompress + # on every commit regardless of caching. Releases keep the slow default. + # Both kinds collect into the one staged dist dir; a drv-only kind + # (full=false) instantiates without adding an ISO. + - name: Build installer + uses: ./.github/actions/build-iso + with: + target: installer + full: ${{ env.INSTALLER_FULL }} + dist: ${{ steps.build.outputs.dist }} + iso-compression: zstd -Xcompression-level 3 + pr-title: ${{ github.event.pull_request.title }} + pr-number: ${{ github.event.pull_request.number }} + branch: ${{ github.head_ref }} + + - name: Build appliance + uses: ./.github/actions/build-iso + with: + target: appliance + full: ${{ env.APPLIANCE_FULL }} + dist: ${{ steps.build.outputs.dist }} + iso-compression: zstd -Xcompression-level 3 + pr-title: ${{ github.event.pull_request.title }} + pr-number: ${{ github.event.pull_request.number }} + branch: ${{ github.head_ref }} # Record the exact byte size of every ISO this arch actually built, one # TSV row per ISO (system, filename, bytes). The iso-table job downloads