From 21d2023df213633bba160f99c7d4d875613df919 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 17:24:40 +0000 Subject: [PATCH 01/16] ci: add installer ISO build + release workflows Add two GitHub Actions workflows: - build.yml: builds make installer/iso for x86_64-linux and aarch64-linux on native runners (push/PR/manual), uploading each ISO as a short-lived artifact. - release.yml: same per-arch build matrix, triggered by a v* tag or manual dispatch, then publishes both ISOs (plus sha256 checksums) as assets on a GitHub Release. --- .github/workflows/build.yml | 76 +++++++++++++++++++++ .github/workflows/release.yml | 125 ++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..747111f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,76 @@ +# Build CI — builds the installer ISO for every supported architecture. +# +# Runs `make installer/iso/` for each arch on a native runner (no QEMU +# emulation: a full ISO build under binfmt is painfully slow). The Makefile +# resolves `make installer/iso/x86_64-linux` etc. and drops a GC-root symlink +# under ./out/installer-iso-/iso/coder-box-installer-.iso. +# +# This is a verification build: it proves both ISOs still build. The artifacts +# are uploaded for inspection but are short-lived; cutting an actual release is +# the release.yml workflow's job. + +name: build + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +# Cancel superseded runs on the same ref (e.g. rapid PR pushes); a full ISO +# build is expensive so don't waste runners on stale commits. +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +jobs: + installer-iso: + name: installer/iso (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64-linux + runner: ubuntu-24.04 + - arch: aarch64-linux + runner: ubuntu-24.04-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v16 + with: + # The Makefile builds via `nix build --impure ... ${builtins.currentSystem}` + # and needs flakes + nix-command enabled. + extra-conf: | + experimental-features = nix-command flakes + accept-flake-config = true + + - name: Cache Nix store + uses: DeterminateSystems/magic-nix-cache-action@v8 + + - name: Build installer ISO + run: make installer/iso/${{ matrix.arch }} + + - name: Resolve ISO path + id: iso + run: | + iso=$(readlink -f "out/installer-iso-${{ matrix.arch }}/iso")/coder-box-installer-${{ matrix.arch }}.iso + if [ ! -f "$iso" ]; then + echo "Expected ISO not found at $iso" >&2 + ls -lR out/ >&2 || true + exit 1 + fi + echo "path=$iso" >>"$GITHUB_OUTPUT" + ls -lh "$iso" + + - name: Upload ISO artifact + uses: actions/upload-artifact@v4 + with: + name: coder-box-installer-${{ matrix.arch }} + path: ${{ steps.iso.outputs.path }} + # Verification build; keep storage cost low. + retention-days: 7 + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..21faae3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,125 @@ +# Release — builds the installer ISO for every supported architecture and +# publishes them as assets on a GitHub Release. +# +# Trigger either way: +# * push a tag matching v* (e.g. `git tag v1.2.0 && git push origin v1.2.0`) +# * run manually from the Actions tab (workflow_dispatch) and supply a tag. +# +# The build job is the same native per-arch `make installer/iso/` matrix +# as build.yml; the release job then gathers both ISOs and attaches them to the +# release for the chosen tag. + +name: release + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Release tag to create/publish (e.g. v1.2.0)" + required: true + type: string + prerelease: + description: "Mark the release as a pre-release" + required: false + default: false + type: boolean + +# Only one release run per tag at a time. +concurrency: + group: release-${{ github.event.inputs.tag || github.ref }} + cancel-in-progress: false + +jobs: + build: + name: installer/iso (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - arch: x86_64-linux + runner: ubuntu-24.04 + - arch: aarch64-linux + runner: ubuntu-24.04-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v16 + with: + extra-conf: | + experimental-features = nix-command flakes + accept-flake-config = true + + - name: Cache Nix store + uses: DeterminateSystems/magic-nix-cache-action@v8 + + - name: Build installer ISO + run: make installer/iso/${{ matrix.arch }} + + - name: Stage ISO for upload + id: iso + run: | + mkdir -p dist + src=$(readlink -f "out/installer-iso-${{ matrix.arch }}/iso")/coder-box-installer-${{ matrix.arch }}.iso + if [ ! -f "$src" ]; then + echo "Expected ISO not found at $src" >&2 + ls -lR out/ >&2 || true + exit 1 + fi + cp "$src" "dist/coder-box-installer-${{ matrix.arch }}.iso" + # Checksum so release consumers can verify the download. + (cd dist && sha256sum "coder-box-installer-${{ matrix.arch }}.iso" \ + >"coder-box-installer-${{ matrix.arch }}.iso.sha256") + ls -lh dist/ + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: coder-box-installer-${{ matrix.arch }} + path: dist/coder-box-installer-${{ matrix.arch }}.iso* + if-no-files-found: error + + release: + name: Publish GitHub Release + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Determine release tag + id: tag + run: | + if [ -n "${{ github.event.inputs.tag }}" ]; then + tag="${{ github.event.inputs.tag }}" + else + tag="${GITHUB_REF#refs/tags/}" + fi + echo "tag=$tag" >>"$GITHUB_OUTPUT" + echo "Releasing tag: $tag" + + - name: Download built ISOs + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: List release assets + run: ls -lhR dist/ + + - name: Create / update GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: ${{ steps.tag.outputs.tag }} + generate_release_notes: true + prerelease: ${{ github.event.inputs.prerelease || false }} + files: | + dist/coder-box-installer-x86_64-linux.iso + dist/coder-box-installer-x86_64-linux.iso.sha256 + dist/coder-box-installer-aarch64-linux.iso + dist/coder-box-installer-aarch64-linux.iso.sha256 + fail_on_unmatched_files: true From d3621ae7144f00897d861ef85be55564eede8faf Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 17:29:42 +0000 Subject: [PATCH 02/16] ci: build inside nixos/nix container via make installer/iso - Build each arch natively (bare `make installer/iso` resolves to the runner's currentSystem) inside the nixos/nix container. - Bring in gnumake + git through nix-shell. - Correct ISO path to out/installer-iso/iso/coder-box-installer--linux.iso. --- .github/workflows/build.yml | 56 +++++++++++++++++------------------ .github/workflows/release.yml | 45 +++++++++++++--------------- 2 files changed, 49 insertions(+), 52 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 747111f..8dbf251 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,11 @@ # Build CI — builds the installer ISO for every supported architecture. # -# Runs `make installer/iso/` for each arch on a native runner (no QEMU -# emulation: a full ISO build under binfmt is painfully slow). The Makefile -# resolves `make installer/iso/x86_64-linux` etc. and drops a GC-root symlink -# under ./out/installer-iso-/iso/coder-box-installer-.iso. +# Each arch is built on a native runner inside the official `nixos/nix` +# container, so `make installer/iso` (no arch suffix) resolves to the runner's +# native `builtins.currentSystem` (x86_64-linux or aarch64-linux). The Makefile +# drops a GC-root symlink at: +# +# out/installer-iso/iso/coder-box-installer--linux.iso # # This is a verification build: it proves both ISOs still build. The artifacts # are uploaded for inspection but are short-lived; cutting an actual release is @@ -25,52 +27,50 @@ concurrency: jobs: installer-iso: - name: installer/iso (${{ matrix.arch }}) + name: installer/iso (${{ matrix.system }}) runs-on: ${{ matrix.runner }} + container: + image: nixos/nix:latest strategy: fail-fast: false matrix: include: - - arch: x86_64-linux + - system: x86_64-linux runner: ubuntu-24.04 - - arch: aarch64-linux + - system: aarch64-linux runner: ubuntu-24.04-arm + env: + # The Makefile builds via `nix build --impure` and needs flakes + + # nix-command enabled; the nixos/nix image ships neither on by default. + NIX_CONFIG: | + experimental-features = nix-command flakes + accept-flake-config = true steps: - name: Checkout uses: actions/checkout@v4 - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v16 - with: - # The Makefile builds via `nix build --impure ... ${builtins.currentSystem}` - # and needs flakes + nix-command enabled. - extra-conf: | - experimental-features = nix-command flakes - accept-flake-config = true - - - name: Cache Nix store - uses: DeterminateSystems/magic-nix-cache-action@v8 - - name: Build installer ISO - run: make installer/iso/${{ matrix.arch }} + # gnumake provides `make`; git lets the Makefile stamp the build rev. + run: nix-shell -p gnumake git --run "make installer/iso" - - name: Resolve ISO path + - name: Stage ISO id: iso run: | - iso=$(readlink -f "out/installer-iso-${{ matrix.arch }}/iso")/coder-box-installer-${{ matrix.arch }}.iso - if [ ! -f "$iso" ]; then - echo "Expected ISO not found at $iso" >&2 + mkdir -p dist + src="out/installer-iso/iso/coder-box-installer-${{ matrix.system }}.iso" + if [ ! -f "$src" ]; then + echo "Expected ISO not found at $src" >&2 ls -lR out/ >&2 || true exit 1 fi - echo "path=$iso" >>"$GITHUB_OUTPUT" - ls -lh "$iso" + cp -L "$src" "dist/coder-box-installer-${{ matrix.system }}.iso" + ls -lh dist/ - name: Upload ISO artifact uses: actions/upload-artifact@v4 with: - name: coder-box-installer-${{ matrix.arch }} - path: ${{ steps.iso.outputs.path }} + name: coder-box-installer-${{ matrix.system }} + path: dist/coder-box-installer-${{ matrix.system }}.iso # Verification build; keep storage cost low. retention-days: 7 if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21faae3..cec9f05 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,9 +5,10 @@ # * push a tag matching v* (e.g. `git tag v1.2.0 && git push origin v1.2.0`) # * run manually from the Actions tab (workflow_dispatch) and supply a tag. # -# The build job is the same native per-arch `make installer/iso/` matrix -# as build.yml; the release job then gathers both ISOs and attaches them to the -# release for the chosen tag. +# Each arch builds on a native runner inside the `nixos/nix` container, so +# `make installer/iso` resolves to the runner's native `builtins.currentSystem` +# and produces out/installer-iso/iso/coder-box-installer--linux.iso. The +# release job then gathers both ISOs and attaches them to the release. name: release @@ -33,54 +34,50 @@ concurrency: jobs: build: - name: installer/iso (${{ matrix.arch }}) + name: installer/iso (${{ matrix.system }}) runs-on: ${{ matrix.runner }} + container: + image: nixos/nix:latest strategy: fail-fast: true matrix: include: - - arch: x86_64-linux + - system: x86_64-linux runner: ubuntu-24.04 - - arch: aarch64-linux + - system: aarch64-linux runner: ubuntu-24.04-arm + env: + NIX_CONFIG: | + experimental-features = nix-command flakes + accept-flake-config = true steps: - name: Checkout uses: actions/checkout@v4 - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v16 - with: - extra-conf: | - experimental-features = nix-command flakes - accept-flake-config = true - - - name: Cache Nix store - uses: DeterminateSystems/magic-nix-cache-action@v8 - - name: Build installer ISO - run: make installer/iso/${{ matrix.arch }} + # gnumake provides `make`; git lets the Makefile stamp the build rev. + run: nix-shell -p gnumake git --run "make installer/iso" - name: Stage ISO for upload - id: iso run: | mkdir -p dist - src=$(readlink -f "out/installer-iso-${{ matrix.arch }}/iso")/coder-box-installer-${{ matrix.arch }}.iso + src="out/installer-iso/iso/coder-box-installer-${{ matrix.system }}.iso" if [ ! -f "$src" ]; then echo "Expected ISO not found at $src" >&2 ls -lR out/ >&2 || true exit 1 fi - cp "$src" "dist/coder-box-installer-${{ matrix.arch }}.iso" + cp -L "$src" "dist/coder-box-installer-${{ matrix.system }}.iso" # Checksum so release consumers can verify the download. - (cd dist && sha256sum "coder-box-installer-${{ matrix.arch }}.iso" \ - >"coder-box-installer-${{ matrix.arch }}.iso.sha256") + (cd dist && sha256sum "coder-box-installer-${{ matrix.system }}.iso" \ + >"coder-box-installer-${{ matrix.system }}.iso.sha256") ls -lh dist/ - name: Upload build artifact uses: actions/upload-artifact@v4 with: - name: coder-box-installer-${{ matrix.arch }} - path: dist/coder-box-installer-${{ matrix.arch }}.iso* + name: coder-box-installer-${{ matrix.system }} + path: dist/coder-box-installer-${{ matrix.system }}.iso* if-no-files-found: error release: From cdf12c975aa815bd0843a9ff6148f1df3c4c1061 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 17:32:41 +0000 Subject: [PATCH 03/16] ci: run nix build via docker run, keep JS actions on host The nixos/nix image has no standard glibc loader, so GitHub's bundled Node fails (exec /__e/node24/bin/node: no such file or directory) when the whole job uses container:. Instead run checkout/upload on the host runner and execute only `make installer/iso` inside the nixos/nix container via docker run, copying the ISO out of the ephemeral container store into the mounted workspace before upload. --- .github/workflows/build.yml | 61 +++++++++++++++++++---------------- .github/workflows/release.yml | 54 ++++++++++++++++--------------- 2 files changed, 62 insertions(+), 53 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8dbf251..d10e932 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,18 @@ # Build CI — builds the installer ISO for every supported architecture. # -# Each arch is built on a native runner inside the official `nixos/nix` -# container, so `make installer/iso` (no arch suffix) resolves to the runner's -# native `builtins.currentSystem` (x86_64-linux or aarch64-linux). The Makefile -# drops a GC-root symlink at: +# Each arch builds on a native runner; the build itself runs inside the +# official `nixos/nix` container via `docker run`. We deliberately do NOT put +# the whole job in `container:` — the nixos/nix image lacks a standard glibc +# loader, so GitHub's bundled Node can't run there and every JS action +# (checkout, upload-artifact) fails with `exec .../node: no such file or +# directory`. So checkout/upload run on the host runner and only `make` runs in +# the container. # -# out/installer-iso/iso/coder-box-installer--linux.iso +# Bare `make installer/iso` resolves to the runner's native +# `builtins.currentSystem` (x86_64-linux or aarch64-linux) and produces +# out/installer-iso/iso/coder-box-installer--linux.iso inside the +# container; we copy it into the mounted workspace (dist/) because the +# container's /nix/store does not persist on the host after it exits. # # This is a verification build: it proves both ISOs still build. The artifacts # are uploaded for inspection but are short-lived; cutting an actual release is @@ -29,8 +36,6 @@ jobs: installer-iso: name: installer/iso (${{ matrix.system }}) runs-on: ${{ matrix.runner }} - container: - image: nixos/nix:latest strategy: fail-fast: false matrix: @@ -39,31 +44,33 @@ jobs: runner: ubuntu-24.04 - system: aarch64-linux runner: ubuntu-24.04-arm - env: - # The Makefile builds via `nix build --impure` and needs flakes + - # nix-command enabled; the nixos/nix image ships neither on by default. - NIX_CONFIG: | - experimental-features = nix-command flakes - accept-flake-config = true steps: - name: Checkout uses: actions/checkout@v4 - - name: Build installer ISO - # gnumake provides `make`; git lets the Makefile stamp the build rev. - run: nix-shell -p gnumake git --run "make installer/iso" - - - name: Stage ISO - id: iso + - name: Build installer ISO (in nixos/nix container) + env: + SYSTEM: ${{ matrix.system }} run: | - mkdir -p dist - src="out/installer-iso/iso/coder-box-installer-${{ matrix.system }}.iso" - if [ ! -f "$src" ]; then - echo "Expected ISO not found at $src" >&2 - ls -lR out/ >&2 || true - exit 1 - fi - cp -L "$src" "dist/coder-box-installer-${{ matrix.system }}.iso" + docker run --rm \ + -v "$PWD":/work -w /work \ + -e SYSTEM \ + nixos/nix:latest \ + sh -euc ' + # The Makefile builds via `nix build --impure` and needs flakes + + # nix-command, which the nixos/nix image does not enable by default. + export NIX_CONFIG="experimental-features = nix-command flakes" + # Repo is bind-mounted and owned by a different uid; allow git to + # read it so the Makefile can stamp the build rev. + git config --global --add safe.directory /work + # gnumake provides `make`; git lets the Makefile stamp the rev. + nix-shell -p gnumake git --run "make installer/iso" + # Copy the ISO out of the (ephemeral) container /nix/store into the + # mounted workspace so the host can upload it. + mkdir -p dist + cp -L "out/installer-iso/iso/coder-box-installer-$SYSTEM.iso" \ + "dist/coder-box-installer-$SYSTEM.iso" + ' ls -lh dist/ - name: Upload ISO artifact diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cec9f05..bcea4c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,10 +5,14 @@ # * push a tag matching v* (e.g. `git tag v1.2.0 && git push origin v1.2.0`) # * run manually from the Actions tab (workflow_dispatch) and supply a tag. # -# Each arch builds on a native runner inside the `nixos/nix` container, so -# `make installer/iso` resolves to the runner's native `builtins.currentSystem` -# and produces out/installer-iso/iso/coder-box-installer--linux.iso. The -# release job then gathers both ISOs and attaches them to the release. +# Like build.yml, each arch builds natively and the build runs inside the +# `nixos/nix` container via `docker run` (the whole job can't use `container:` +# because that image lacks a glibc loader for GitHub's bundled Node, which +# breaks every JS action). Bare `make installer/iso` resolves to the runner's +# native currentSystem and produces +# out/installer-iso/iso/coder-box-installer--linux.iso, which we copy out +# of the ephemeral container store into the mounted workspace. The release job +# then gathers both ISOs and attaches them to the release. name: release @@ -36,8 +40,6 @@ jobs: build: name: installer/iso (${{ matrix.system }}) runs-on: ${{ matrix.runner }} - container: - image: nixos/nix:latest strategy: fail-fast: true matrix: @@ -46,31 +48,31 @@ jobs: runner: ubuntu-24.04 - system: aarch64-linux runner: ubuntu-24.04-arm - env: - NIX_CONFIG: | - experimental-features = nix-command flakes - accept-flake-config = true steps: - name: Checkout uses: actions/checkout@v4 - - name: Build installer ISO - # gnumake provides `make`; git lets the Makefile stamp the build rev. - run: nix-shell -p gnumake git --run "make installer/iso" - - - name: Stage ISO for upload + - name: Build installer ISO (in nixos/nix container) + env: + SYSTEM: ${{ matrix.system }} run: | - mkdir -p dist - src="out/installer-iso/iso/coder-box-installer-${{ matrix.system }}.iso" - if [ ! -f "$src" ]; then - echo "Expected ISO not found at $src" >&2 - ls -lR out/ >&2 || true - exit 1 - fi - cp -L "$src" "dist/coder-box-installer-${{ matrix.system }}.iso" - # Checksum so release consumers can verify the download. - (cd dist && sha256sum "coder-box-installer-${{ matrix.system }}.iso" \ - >"coder-box-installer-${{ matrix.system }}.iso.sha256") + docker run --rm \ + -v "$PWD":/work -w /work \ + -e SYSTEM \ + nixos/nix:latest \ + sh -euc ' + export NIX_CONFIG="experimental-features = nix-command flakes" + git config --global --add safe.directory /work + # gnumake provides `make`; git lets the Makefile stamp the rev. + nix-shell -p gnumake git --run "make installer/iso" + # Copy the ISO out of the (ephemeral) container /nix/store and + # checksum it so release consumers can verify the download. + mkdir -p dist + cp -L "out/installer-iso/iso/coder-box-installer-$SYSTEM.iso" \ + "dist/coder-box-installer-$SYSTEM.iso" + cd dist && sha256sum "coder-box-installer-$SYSTEM.iso" \ + >"coder-box-installer-$SYSTEM.iso.sha256" + ' ls -lh dist/ - name: Upload build artifact From 93a4eff0379c4aa586d1d42d42de70d02dbbc01f Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 17:55:40 +0000 Subject: [PATCH 04/16] ci: add appliance ISO, gate PR builds on ready-for-review, v5 actions - Build both installer/iso and appliance/iso per arch (matrix target). - Bump checkout/upload/download actions to v5 (Node 24; clears the Node 20 deprecation warning). - Rename workflows: 'Build ISO' and 'Build and release ISO'. - PR builds only on ready-for-review (opened/reopened/ready_for_review, no synchronize) + draft guard; still manually dispatchable for an arbitrary ref/commit. - Upload ISOs from a host mktemp dir bind-mounted into the container instead of a tracked dist/ folder. - build.yml artifact retention dropped to 1 day. --- .github/workflows/build.yml | 97 ++++++++++++++++++++++------------- .github/workflows/release.yml | 82 +++++++++++++++++------------ 2 files changed, 110 insertions(+), 69 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d10e932..4d545a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,5 @@ -# Build CI — builds the installer ISO for every supported architecture. +# Build ISO — builds the installer AND appliance ISOs for every supported +# architecture. # # Each arch builds on a native runner; the build itself runs inside the # official `nixos/nix` container via `docker run`. We deliberately do NOT put @@ -8,37 +9,51 @@ # directory`. So checkout/upload run on the host runner and only `make` runs in # the container. # -# Bare `make installer/iso` resolves to the runner's native -# `builtins.currentSystem` (x86_64-linux or aarch64-linux) and produces -# out/installer-iso/iso/coder-box-installer--linux.iso inside the -# container; we copy it into the mounted workspace (dist/) because the -# container's /nix/store does not persist on the host after it exits. +# Bare `make ` resolves to the runner's native `builtins.currentSystem` +# (x86_64-linux or aarch64-linux) and produces +# out//iso/coder-box---linux.iso inside the container. The +# container /nix/store is ephemeral, so we copy the ISOs into a host mktemp dir +# bind-mounted at /out and upload from there. # -# This is a verification build: it proves both ISOs still build. The artifacts -# are uploaded for inspection but are short-lived; cutting an actual release is -# the release.yml workflow's job. +# Triggers: builds for PRs only when they're ready for review (not on every +# commit/draft push), on merges to main, and manually for any ref via +# workflow_dispatch. Verification build: artifacts are short-lived (1 day). -name: build +name: Build ISO on: push: branches: [main] pull_request: + # Only ready-for-review PRs: `opened`/`reopened` cover a PR created or + # reopened already non-draft, `ready_for_review` covers a draft promoted to + # ready. Notably absent is `synchronize`, so pushing new commits to an open + # PR does NOT rebuild — the draft guard below also skips draft PRs. + types: [opened, reopened, ready_for_review] workflow_dispatch: + inputs: + ref: + description: "Git ref/commit to build (defaults to the selected branch)" + required: false + type: string -# Cancel superseded runs on the same ref (e.g. rapid PR pushes); a full ISO -# build is expensive so don't waste runners on stale commits. +# Cancel superseded runs on the same ref; a full ISO build is expensive so +# don't waste runners on stale commits. concurrency: - group: build-${{ github.ref }} + group: build-${{ github.ref }}-${{ github.event.inputs.ref }} cancel-in-progress: true jobs: - installer-iso: - name: installer/iso (${{ matrix.system }}) + iso: + name: ${{ matrix.target }} (${{ matrix.system }}) + # Skip draft PRs (opened/reopened fire for drafts too); always run for + # push and manual dispatch. + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: + target: [installer/iso, appliance/iso] include: - system: x86_64-linux runner: ubuntu-24.04 @@ -46,38 +61,50 @@ jobs: runner: ubuntu-24.04-arm steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 + with: + # Empty for push/PR (checks out the event ref); honored for manual + # dispatch to build an arbitrary commit. + ref: ${{ github.event.inputs.ref }} - - name: Build installer ISO (in nixos/nix container) + - name: Build ISO (in nixos/nix container) + id: build env: + TARGET: ${{ matrix.target }} SYSTEM: ${{ matrix.system }} run: | + # Host temp dir, bind-mounted into the container, where the build + # drops the finished ISO (the container's /nix/store is ephemeral). + outdir="$(mktemp -d)" + echo "outdir=$outdir" >>"$GITHUB_OUTPUT" + # out/ -> dir>/... e.g. installer/iso -> out/installer-iso + outlink="out/$(echo "$TARGET" | tr '/' '-')/iso" + # kind = installer | appliance (first path component of the target). + kind="${TARGET%%/*}" docker run --rm \ -v "$PWD":/work -w /work \ - -e SYSTEM \ + -v "$outdir":/out \ + -e TARGET -e SYSTEM \ nixos/nix:latest \ - sh -euc ' - # The Makefile builds via `nix build --impure` and needs flakes + + sh -euc " + # The Makefile builds via 'nix build --impure' and needs flakes + # nix-command, which the nixos/nix image does not enable by default. - export NIX_CONFIG="experimental-features = nix-command flakes" + export NIX_CONFIG='experimental-features = nix-command flakes' # Repo is bind-mounted and owned by a different uid; allow git to # read it so the Makefile can stamp the build rev. git config --global --add safe.directory /work - # gnumake provides `make`; git lets the Makefile stamp the rev. - nix-shell -p gnumake git --run "make installer/iso" - # Copy the ISO out of the (ephemeral) container /nix/store into the - # mounted workspace so the host can upload it. - mkdir -p dist - cp -L "out/installer-iso/iso/coder-box-installer-$SYSTEM.iso" \ - "dist/coder-box-installer-$SYSTEM.iso" - ' - ls -lh dist/ + # gnumake provides 'make'; git lets the Makefile stamp the rev. + nix-shell -p gnumake git --run \"make $TARGET\" + cp -L '$outlink/coder-box-$kind-$SYSTEM.iso' \ + '/out/coder-box-$kind-$SYSTEM.iso' + " + ls -lh "$outdir" - name: Upload ISO artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: - name: coder-box-installer-${{ matrix.system }} - path: dist/coder-box-installer-${{ matrix.system }}.iso - # Verification build; keep storage cost low. - retention-days: 7 + name: coder-box-${{ matrix.target == 'installer/iso' && 'installer' || 'appliance' }}-${{ matrix.system }} + path: ${{ steps.build.outputs.outdir }}/*.iso + # Verification build; keep storage cost minimal. + retention-days: 1 if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bcea4c2..adcac29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,20 @@ -# Release — builds the installer ISO for every supported architecture and -# publishes them as assets on a GitHub Release. +# Build and release ISO — builds the installer AND appliance ISOs for every +# supported architecture and publishes them as assets on a GitHub Release. # # Trigger either way: # * push a tag matching v* (e.g. `git tag v1.2.0 && git push origin v1.2.0`) -# * run manually from the Actions tab (workflow_dispatch) and supply a tag. +# * run manually from the Actions tab (workflow_dispatch), supplying a tag +# and optionally a ref/commit to build. # # Like build.yml, each arch builds natively and the build runs inside the # `nixos/nix` container via `docker run` (the whole job can't use `container:` # because that image lacks a glibc loader for GitHub's bundled Node, which -# breaks every JS action). Bare `make installer/iso` resolves to the runner's -# native currentSystem and produces -# out/installer-iso/iso/coder-box-installer--linux.iso, which we copy out -# of the ephemeral container store into the mounted workspace. The release job -# then gathers both ISOs and attaches them to the release. +# breaks every JS action). Bare `make ` resolves to the runner's native +# currentSystem; the resulting ISO is copied out of the ephemeral container +# store into a host mktemp dir. The release job gathers all ISOs and attaches +# them (with sha256 checksums) to the release. -name: release +name: Build and release ISO on: push: @@ -25,6 +25,10 @@ on: description: "Release tag to create/publish (e.g. v1.2.0)" required: true type: string + ref: + description: "Git ref/commit to build (defaults to the tag/branch)" + required: false + type: string prerelease: description: "Mark the release as a pre-release" required: false @@ -38,11 +42,12 @@ concurrency: jobs: build: - name: installer/iso (${{ matrix.system }}) + name: ${{ matrix.target }} (${{ matrix.system }}) runs-on: ${{ matrix.runner }} strategy: fail-fast: true matrix: + target: [installer/iso, appliance/iso] include: - system: x86_64-linux runner: ubuntu-24.04 @@ -50,36 +55,47 @@ jobs: runner: ubuntu-24.04-arm steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 + with: + # Empty for tag push (checks out the tag); honored for manual dispatch + # to release an arbitrary commit. + ref: ${{ github.event.inputs.ref }} - - name: Build installer ISO (in nixos/nix container) + - name: Build ISO (in nixos/nix container) + id: build env: + TARGET: ${{ matrix.target }} SYSTEM: ${{ matrix.system }} run: | + # Host temp dir, bind-mounted into the container, where the build + # drops the finished ISO (the container's /nix/store is ephemeral). + outdir="$(mktemp -d)" + echo "outdir=$outdir" >>"$GITHUB_OUTPUT" + outlink="out/$(echo "$TARGET" | tr '/' '-')/iso" + kind="${TARGET%%/*}" docker run --rm \ -v "$PWD":/work -w /work \ - -e SYSTEM \ + -v "$outdir":/out \ + -e TARGET -e SYSTEM \ nixos/nix:latest \ - sh -euc ' - export NIX_CONFIG="experimental-features = nix-command flakes" + sh -euc " + export NIX_CONFIG='experimental-features = nix-command flakes' git config --global --add safe.directory /work - # gnumake provides `make`; git lets the Makefile stamp the rev. - nix-shell -p gnumake git --run "make installer/iso" - # Copy the ISO out of the (ephemeral) container /nix/store and - # checksum it so release consumers can verify the download. - mkdir -p dist - cp -L "out/installer-iso/iso/coder-box-installer-$SYSTEM.iso" \ - "dist/coder-box-installer-$SYSTEM.iso" - cd dist && sha256sum "coder-box-installer-$SYSTEM.iso" \ - >"coder-box-installer-$SYSTEM.iso.sha256" - ' - ls -lh dist/ + # gnumake provides 'make'; git lets the Makefile stamp the rev. + nix-shell -p gnumake git --run \"make $TARGET\" + cp -L '$outlink/coder-box-$kind-$SYSTEM.iso' \ + '/out/coder-box-$kind-$SYSTEM.iso' + # Checksum so release consumers can verify the download. + cd /out && sha256sum 'coder-box-$kind-$SYSTEM.iso' \ + >'coder-box-$kind-$SYSTEM.iso.sha256' + " + ls -lh "$outdir" - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: - name: coder-box-installer-${{ matrix.system }} - path: dist/coder-box-installer-${{ matrix.system }}.iso* + name: coder-box-${{ matrix.target == 'installer/iso' && 'installer' || 'appliance' }}-${{ matrix.system }} + path: ${{ steps.build.outputs.outdir }}/*.iso* if-no-files-found: error release: @@ -101,7 +117,7 @@ jobs: echo "Releasing tag: $tag" - name: Download built ISOs - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: path: dist merge-multiple: true @@ -117,8 +133,6 @@ jobs: generate_release_notes: true prerelease: ${{ github.event.inputs.prerelease || false }} files: | - dist/coder-box-installer-x86_64-linux.iso - dist/coder-box-installer-x86_64-linux.iso.sha256 - dist/coder-box-installer-aarch64-linux.iso - dist/coder-box-installer-aarch64-linux.iso.sha256 + dist/*.iso + dist/*.iso.sha256 fail_on_unmatched_files: true From 0b6cbe476226a7ccf955b9b97c988395611fb495 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 17:57:19 +0000 Subject: [PATCH 05/16] ci: add Eval Nix workflow to instantiate installer ISO drv on every commit/PR Runs on every push and pull_request (cheap, unlike the gated full image builds). Evaluates the flake and instantiates the installer ISO derivation for both arches via 'nix eval ... .drvPath' (writes the .drv, no realisation), catching Nix typos/type errors in seconds without building the multi-GB ISO. Both arches instantiate on one x86_64 runner since evaluation needs no native builder; eval runs in the nixos/nix container via docker run, checkout stays on the host. --- .github/workflows/eval.yml | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/eval.yml diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml new file mode 100644 index 0000000..8adda03 --- /dev/null +++ b/.github/workflows/eval.yml @@ -0,0 +1,60 @@ +# Eval Nix — fast validation that the flake still evaluates and the installer +# ISO derivation instantiates. +# +# Unlike Build ISO / Build and release ISO (which realise the full multi-GB +# image and only run for ready PRs / tags), this is cheap and runs on EVERY +# commit and pull request. It instantiates the installer ISO derivation — i.e. +# fully evaluates the Nix and writes the .drv to the store — but never builds +# it (`nix eval ... .drvPath` forces instantiation without realisation). A +# typo, bad reference, or type error in the Nix surfaces here in seconds +# instead of after a long image build. +# +# Both architectures are instantiated on a single x86_64 runner: evaluating an +# aarch64-linux derivation needs no aarch64 builder (only realisation would). +# The eval runs inside the nixos/nix container via `docker run` for the same +# reason as the other workflows — that image lacks a glibc loader for GitHub's +# bundled Node, so JS actions (checkout) stay on the host runner. + +name: Eval Nix + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: eval-${{ github.ref }} + cancel-in-progress: true + +jobs: + eval: + name: instantiate installer/iso + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Instantiate installer ISO derivation (in nixos/nix container) + run: | + docker run --rm \ + -v "$PWD":/work -w /work \ + nixos/nix:latest \ + sh -euc ' + export NIX_CONFIG="experimental-features = nix-command flakes" + git config --global --add safe.directory /work + # Mirror the Makefile build expr (path flakeref + per-arch + # hostPlatform override), but evaluate .drvPath so Nix + # instantiates the derivation (writes the .drv) WITHOUT building + # the ISO. Done for both supported architectures. + for system in x86_64-linux aarch64-linux; do + echo "==> instantiating installer/iso for $system" + drv=$(nix eval --impure --raw --expr " + let f = builtins.getFlake (toString ./.); in + (f.nixosConfigurations._installer-iso.extendModules { + modules = [ { nixpkgs.hostPlatform = \"$system\"; } ]; + }).config.system.build.isoImage.drvPath + ") + echo " $drv" + done + ' From e051d04b1af9097958b3b6d8aaeb95560ecf1c71 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 18:04:06 +0000 Subject: [PATCH 06/16] ci: make Eval Nix run on PR synchronize explicitly Bare pull_request already defaults to opened/synchronize/reopened; list the types explicitly so per-commit re-eval intent is clear. --- .github/workflows/eval.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 8adda03..33de80d 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -21,6 +21,9 @@ on: push: branches: [main] pull_request: + # Re-eval on every PR commit too (synchronize), plus open/reopen. This is + # the cheap per-commit gate, so unlike Build ISO we DO want synchronize. + types: [opened, reopened, synchronize] workflow_dispatch: concurrency: From b9e3356d5d104470caaf18d6e98c56bf2f07db29 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 18:07:11 +0000 Subject: [PATCH 07/16] make: add installer/drv + appliance/drv instantiate-only targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a box_instantiate helper (box_build's sibling) that evaluates the image derivation's .drvPath — fully evaluating the Nix and writing the .drv to the store without realising the multi-GB image. Expose it as installer/drv and appliance/drv (bare + arch-suffixed). Eval Nix workflow now just calls these make targets for both arches instead of an inline nix eval expression. --- .github/workflows/eval.yml | 40 +++++++++++++++----------------------- Makefile | 33 ++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 33de80d..f6ffb76 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -1,13 +1,13 @@ -# Eval Nix — fast validation that the flake still evaluates and the installer -# ISO derivation instantiates. +# Eval Nix — fast validation that the flake still evaluates and the ISO +# derivations instantiate. # # Unlike Build ISO / Build and release ISO (which realise the full multi-GB -# image and only run for ready PRs / tags), this is cheap and runs on EVERY -# commit and pull request. It instantiates the installer ISO derivation — i.e. -# fully evaluates the Nix and writes the .drv to the store — but never builds -# it (`nix eval ... .drvPath` forces instantiation without realisation). A -# typo, bad reference, or type error in the Nix surfaces here in seconds -# instead of after a long image build. +# images and only run for ready PRs / tags), this is cheap and runs on EVERY +# commit and pull request. It instantiates the installer + appliance ISO +# derivations via the Makefile's `*/drv` targets — i.e. fully evaluates the Nix +# and writes the .drv to the store, but never builds the image. A typo, bad +# reference, or type error in the Nix surfaces here in seconds instead of after +# a long image build. # # Both architectures are instantiated on a single x86_64 runner: evaluating an # aarch64-linux derivation needs no aarch64 builder (only realisation would). @@ -32,13 +32,13 @@ concurrency: jobs: eval: - name: instantiate installer/iso + name: instantiate ISO derivations runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v5 - - name: Instantiate installer ISO derivation (in nixos/nix container) + - name: Instantiate ISO derivations (in nixos/nix container) run: | docker run --rm \ -v "$PWD":/work -w /work \ @@ -46,18 +46,10 @@ jobs: sh -euc ' export NIX_CONFIG="experimental-features = nix-command flakes" git config --global --add safe.directory /work - # Mirror the Makefile build expr (path flakeref + per-arch - # hostPlatform override), but evaluate .drvPath so Nix - # instantiates the derivation (writes the .drv) WITHOUT building - # the ISO. Done for both supported architectures. - for system in x86_64-linux aarch64-linux; do - echo "==> instantiating installer/iso for $system" - drv=$(nix eval --impure --raw --expr " - let f = builtins.getFlake (toString ./.); in - (f.nixosConfigurations._installer-iso.extendModules { - modules = [ { nixpkgs.hostPlatform = \"$system\"; } ]; - }).config.system.build.isoImage.drvPath - ") - echo " $drv" - done + # `*/drv` targets evaluate .drvPath (instantiate without build). + # Do both arches for each image; aarch64 eval needs no builder. + nix-shell -p gnumake git --run " + make installer/drv/x86_64-linux installer/drv/aarch64-linux \ + appliance/drv/x86_64-linux appliance/drv/aarch64-linux + " ' diff --git a/Makefile b/Makefile index ab207f4..561ceed 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,13 @@ # # make installer/iso # +# For cheap validation (CI, quick "does the Nix evaluate?" checks) there are +# instantiate-only targets that evaluate the derivation and write its .drv to +# the store WITHOUT building the multi-GB image: +# +# make installer/drv +# make appliance/drv +# # Each target also takes an architecture suffix; short names are normalized to # a *-linux triple (e.g. aarch64 -> aarch64-linux): # @@ -70,7 +77,19 @@ define box_build 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(4),$(call norm_arch,$(4)),$${builtins.currentSystem})"; coderBox.rev = "$(GIT_REV)"; $(3) } ]; }).config.system.build.$(2)' endef -.PHONY: installer/iso appliance/iso appliance/qcow2 appliance/raw +# Instantiate-only counterpart to box_build: same flake expr, but evaluates +# `.drvPath` so Nix fully evaluates the config and writes the .drv to the store +# WITHOUT realising the (multi-GB) image. Cheap CI validation that the Nix is +# sound. Prints the resulting store .drv path. No ./out GC-root link: there's no +# built output to anchor, and the .drv itself is a GC root until next gc. +# $(1) = host $(2) = system.build. $(3) = extra module fields $(4) = arch token +define box_instantiate + $(NIX) eval --impure --no-write-lock-file --raw --expr \ + 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(4),$(call norm_arch,$(4)),$${builtins.currentSystem})"; coderBox.rev = "$(GIT_REV)"; $(3) } ]; }).config.system.build.$(2).drvPath' + @echo +endef + +.PHONY: installer/iso installer/drv appliance/iso appliance/drv appliance/qcow2 appliance/raw # installer/iso is listed first so it's the default goal (bare `make`). @@ -80,12 +99,24 @@ installer/iso: installer/iso/%: $(call box_build,_installer-iso,isoImage,,$*) +# ── installer/drv — instantiate the installer ISO derivation (no build) ─────── +installer/drv: + $(call box_instantiate,_installer-iso,isoImage,,) +installer/drv/%: + $(call box_instantiate,_installer-iso,isoImage,,$*) + # ── appliance/iso — ephemeral appliance ISO (hosts/_appliance_iso) ─────────── appliance/iso: $(call box_build,_appliance_iso,isoImage,,) appliance/iso/%: $(call box_build,_appliance_iso,isoImage,,$*) +# ── appliance/drv — instantiate the appliance ISO derivation (no build) ─────── +appliance/drv: + $(call box_instantiate,_appliance_iso,isoImage,,) +appliance/drv/%: + $(call box_instantiate,_appliance_iso,isoImage,,$*) + # ── appliance/qcow2 — persistent disk image (hosts/_appliance-disk) ────────── appliance/qcow2: $(call box_build,_appliance-disk,diskoImages,disko.imageBuilder.imageFormat = "qcow2";,) From ffe43a866bffac24c29968d3ac1e2fbe7d2a7203 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 18:08:31 +0000 Subject: [PATCH 08/16] make: factor shared flake expr into box_cfg box_build and box_instantiate duplicated the entire 'let f = builtins.getFlake ... config.system.build' expression. Extract it into a box_cfg $(call) function so the two can't drift; box_build appends the build attr, box_instantiate appends .drvPath. Verified the expanded recipes are byte-identical to before via make --dry-run. --- Makefile | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 561ceed..fdc3f56 100644 --- a/Makefile +++ b/Makefile @@ -70,11 +70,21 @@ norm_arch = $(if $(filter %-linux,$(1)),$(1),$(1)-linux) # native, non-copy way to surface the result in the repo: ./out/ points # straight at the store path, and being a GC root it won't be garbage-collected. # ./out is gitignored. +# The shared flake expression, factored out so box_build and box_instantiate +# can't drift apart. Selects `config.system.build` for the configured system; +# callers append the build attr (and, for instantiate, `.drvPath`). This is the +# per-arch override described above (always-pinned hostPlatform; native arch via +# currentSystem when no token is given; Makefile-injected coderBox.rev). +# $(1) = host (nixosConfigurations.) +# $(2) = arch token (empty = builder's native arch) +# $(3) = extra module fields (nix attrset body, may be empty) +box_cfg = let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(2),$(call norm_arch,$(2)),$${builtins.currentSystem})"; coderBox.rev = "$(GIT_REV)"; $(3) } ]; }).config.system.build + define box_build @mkdir -p out $(NIX) build --impure --no-write-lock-file --print-out-paths \ --out-link 'out/$(subst /,-,$@)' --expr \ - 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(4),$(call norm_arch,$(4)),$${builtins.currentSystem})"; coderBox.rev = "$(GIT_REV)"; $(3) } ]; }).config.system.build.$(2)' + '$(call box_cfg,$(1),$(4),$(3)).$(2)' endef # Instantiate-only counterpart to box_build: same flake expr, but evaluates @@ -85,7 +95,7 @@ endef # $(1) = host $(2) = system.build. $(3) = extra module fields $(4) = arch token define box_instantiate $(NIX) eval --impure --no-write-lock-file --raw --expr \ - 'let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(4),$(call norm_arch,$(4)),$${builtins.currentSystem})"; coderBox.rev = "$(GIT_REV)"; $(3) } ]; }).config.system.build.$(2).drvPath' + '$(call box_cfg,$(1),$(4),$(3)).$(2).drvPath' @echo endef From 67916695c9537849ee67eaa75102e5f829142920 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 18:12:46 +0000 Subject: [PATCH 09/16] ci: also run nix flake check in Eval Nix Eval Nix now does two no-build checks: nix flake check --no-build --all-systems (evaluates every flake output for all declared systems) followed by the */drv ISO derivation instantiation. Reworded a comment to drop an apostrophe that would break the single-quoted docker-run shell block. --- .github/workflows/eval.yml | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index f6ffb76..a10722d 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -3,11 +3,13 @@ # # Unlike Build ISO / Build and release ISO (which realise the full multi-GB # images and only run for ready PRs / tags), this is cheap and runs on EVERY -# commit and pull request. It instantiates the installer + appliance ISO -# derivations via the Makefile's `*/drv` targets — i.e. fully evaluates the Nix -# and writes the .drv to the store, but never builds the image. A typo, bad -# reference, or type error in the Nix surfaces here in seconds instead of after -# a long image build. +# commit and pull request. Two checks, neither builds anything: +# 1. `nix flake check --no-build --all-systems` — evaluates every flake +# output (nixosConfigurations, packages, …) for all declared systems. +# 2. The Makefile's `*/drv` targets instantiate the installer + appliance ISO +# derivations (evaluate .drvPath, write the .drv, no realisation). +# A typo, bad reference, or type error in the Nix surfaces here in seconds +# instead of after a long image build. # # Both architectures are instantiated on a single x86_64 runner: evaluating an # aarch64-linux derivation needs no aarch64 builder (only realisation would). @@ -38,7 +40,7 @@ jobs: - name: Checkout uses: actions/checkout@v5 - - name: Instantiate ISO derivations (in nixos/nix container) + - name: Eval flake + instantiate ISO derivations (in nixos/nix container) run: | docker run --rm \ -v "$PWD":/work -w /work \ @@ -46,8 +48,17 @@ jobs: sh -euc ' export NIX_CONFIG="experimental-features = nix-command flakes" git config --global --add safe.directory /work - # `*/drv` targets evaluate .drvPath (instantiate without build). - # Do both arches for each image; aarch64 eval needs no builder. + + # 1) Evaluate every flake output (nixosConfigurations, packages, + # etc.) without realising anything: --no-build skips building + # derivations, --all-systems checks outputs for every system + # the flake declares (x86_64 + aarch64). Catches eval errors in + # flake outputs not covered by the ISO drv targets below. + nix flake check --impure --no-build --all-systems + + # 2) Instantiate the ISO derivations via the `*/drv` make targets + # (evaluate .drvPath, no build). Both arches per image; aarch64 + # eval needs no aarch64 builder. nix-shell -p gnumake git --run " make installer/drv/x86_64-linux installer/drv/aarch64-linux \ appliance/drv/x86_64-linux appliance/drv/aarch64-linux From cd2839d31c8761d906ce6fbca75f254f0ec54518 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 18:17:06 +0000 Subject: [PATCH 10/16] ci: bind-mount a host /dist volume for ISO output Replace the explicit two-arg 'cp /out/coder-box--.iso' with a /dist volume (host mktemp dir) bind-mounted into the container; the build dereferences its store symlink into /dist via 'cp -L $outlink/*.iso /dist/'. This drops the hardcoded filename (and the now-unused kind/SYSTEM shell vars + env), and uploads from /dist. Release computes per-file sha256 over /dist/*.iso. --- .github/workflows/build.yml | 25 ++++++++++++------------- .github/workflows/release.yml | 29 ++++++++++++++--------------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d545a4..2fd9ff0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,20 +71,19 @@ jobs: id: build env: TARGET: ${{ matrix.target }} - SYSTEM: ${{ matrix.system }} run: | - # Host temp dir, bind-mounted into the container, where the build - # drops the finished ISO (the container's /nix/store is ephemeral). - outdir="$(mktemp -d)" - echo "outdir=$outdir" >>"$GITHUB_OUTPUT" + # Host /dist volume, bind-mounted into the container, where the build + # drops the finished ISO (the container's /nix/store is ephemeral, so + # the in-tree out/ symlink would dangle on the host — we dereference + # the built ISO into /dist instead). + dist="$(mktemp -d)" + echo "dist=$dist" >>"$GITHUB_OUTPUT" # out/ -> dir>/... e.g. installer/iso -> out/installer-iso outlink="out/$(echo "$TARGET" | tr '/' '-')/iso" - # kind = installer | appliance (first path component of the target). - kind="${TARGET%%/*}" docker run --rm \ -v "$PWD":/work -w /work \ - -v "$outdir":/out \ - -e TARGET -e SYSTEM \ + -v "$dist":/dist \ + -e TARGET \ nixos/nix:latest \ sh -euc " # The Makefile builds via 'nix build --impure' and needs flakes + @@ -95,16 +94,16 @@ jobs: git config --global --add safe.directory /work # gnumake provides 'make'; git lets the Makefile stamp the rev. nix-shell -p gnumake git --run \"make $TARGET\" - cp -L '$outlink/coder-box-$kind-$SYSTEM.iso' \ - '/out/coder-box-$kind-$SYSTEM.iso' + # Dereference the build's store symlink into the /dist volume. + cp -L $outlink/*.iso /dist/ " - ls -lh "$outdir" + ls -lh "$dist" - name: Upload ISO artifact uses: actions/upload-artifact@v5 with: name: coder-box-${{ matrix.target == 'installer/iso' && 'installer' || 'appliance' }}-${{ matrix.system }} - path: ${{ steps.build.outputs.outdir }}/*.iso + path: ${{ steps.build.outputs.dist }}/*.iso # Verification build; keep storage cost minimal. retention-days: 1 if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index adcac29..fab890c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,37 +65,36 @@ jobs: id: build env: TARGET: ${{ matrix.target }} - SYSTEM: ${{ matrix.system }} run: | - # Host temp dir, bind-mounted into the container, where the build - # drops the finished ISO (the container's /nix/store is ephemeral). - outdir="$(mktemp -d)" - echo "outdir=$outdir" >>"$GITHUB_OUTPUT" + # Host /dist volume, bind-mounted into the container, where the build + # drops the finished ISO (the container's /nix/store is ephemeral, so + # the in-tree out/ symlink would dangle on the host — we dereference + # the built ISO into /dist instead). + dist="$(mktemp -d)" + echo "dist=$dist" >>"$GITHUB_OUTPUT" outlink="out/$(echo "$TARGET" | tr '/' '-')/iso" - kind="${TARGET%%/*}" docker run --rm \ -v "$PWD":/work -w /work \ - -v "$outdir":/out \ - -e TARGET -e SYSTEM \ + -v "$dist":/dist \ + -e TARGET \ nixos/nix:latest \ sh -euc " export NIX_CONFIG='experimental-features = nix-command flakes' git config --global --add safe.directory /work # gnumake provides 'make'; git lets the Makefile stamp the rev. nix-shell -p gnumake git --run \"make $TARGET\" - cp -L '$outlink/coder-box-$kind-$SYSTEM.iso' \ - '/out/coder-box-$kind-$SYSTEM.iso' - # Checksum so release consumers can verify the download. - cd /out && sha256sum 'coder-box-$kind-$SYSTEM.iso' \ - >'coder-box-$kind-$SYSTEM.iso.sha256' + # Dereference the build's store symlink into the /dist volume, then + # checksum it so release consumers can verify the download. + cp -L $outlink/*.iso /dist/ + cd /dist && for f in *.iso; do sha256sum \"\$f\" >\"\$f.sha256\"; done " - ls -lh "$outdir" + ls -lh "$dist" - name: Upload build artifact uses: actions/upload-artifact@v5 with: name: coder-box-${{ matrix.target == 'installer/iso' && 'installer' || 'appliance' }}-${{ matrix.system }} - path: ${{ steps.build.outputs.outdir }}/*.iso* + path: ${{ steps.build.outputs.dist }}/*.iso* if-no-files-found: error release: From a8399a9f0fe2d109ae8bc5705004d3af484c2716 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 18:21:51 +0000 Subject: [PATCH 11/16] make: generate ISO sha256 sidecars in the build targets Add a box_iso helper that runs box_build then writes out/.iso.sha256 (bare basename, so 'sha256sum -c' verifies against the ISO beside it). installer/iso and appliance/iso now use it. Both workflows drop their inline sha256sum and just copy the make-produced sidecar from out/ into the /dist volume. --- .github/workflows/build.yml | 9 ++++++--- .github/workflows/release.yml | 10 ++++++---- Makefile | 24 ++++++++++++++++++++---- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2fd9ff0..124de28 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -92,10 +92,13 @@ jobs: # Repo is bind-mounted and owned by a different uid; allow git to # read it so the Makefile can stamp the build rev. git config --global --add safe.directory /work - # gnumake provides 'make'; git lets the Makefile stamp the rev. + # gnumake provides 'make'; git lets the Makefile stamp the rev. The + # ISO targets also emit out/.iso.sha256 sidecar checksums. nix-shell -p gnumake git --run \"make $TARGET\" - # Dereference the build's store symlink into the /dist volume. + # Dereference the build's store symlink into the /dist volume, + # alongside the make-generated checksum sidecar. cp -L $outlink/*.iso /dist/ + cp out/*.iso.sha256 /dist/ " ls -lh "$dist" @@ -103,7 +106,7 @@ jobs: uses: actions/upload-artifact@v5 with: name: coder-box-${{ matrix.target == 'installer/iso' && 'installer' || 'appliance' }}-${{ matrix.system }} - path: ${{ steps.build.outputs.dist }}/*.iso + path: ${{ steps.build.outputs.dist }}/*.iso* # Verification build; keep storage cost minimal. retention-days: 1 if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fab890c..6b7bfb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,12 +81,14 @@ jobs: sh -euc " export NIX_CONFIG='experimental-features = nix-command flakes' git config --global --add safe.directory /work - # gnumake provides 'make'; git lets the Makefile stamp the rev. + # gnumake provides 'make'; git lets the Makefile stamp the rev. The + # ISO targets also emit out/.iso.sha256 sidecar checksums so + # release consumers can verify the download. nix-shell -p gnumake git --run \"make $TARGET\" - # Dereference the build's store symlink into the /dist volume, then - # checksum it so release consumers can verify the download. + # Dereference the build's store symlink into the /dist volume, + # alongside the make-generated checksum sidecar. cp -L $outlink/*.iso /dist/ - cd /dist && for f in *.iso; do sha256sum \"\$f\" >\"\$f.sha256\"; done + cp out/*.iso.sha256 /dist/ " ls -lh "$dist" diff --git a/Makefile b/Makefile index fdc3f56..31d5c78 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,22 @@ define box_build '$(call box_cfg,$(1),$(4),$(3)).$(2)' endef +# ISO build helper: box_build, then emit a SHA-256 sidecar for each produced +# ISO. iso-image.nix lands the image under /iso/.iso, which +# lives in the read-only /nix/store; we write the checksum into the writable +# ./out dir as out/.iso.sha256, with the bare basename (no store path) so +# `sha256sum -c` verifies against the ISO sitting next to it. Same arg shape as +# box_build (always $(2)=isoImage here). +define box_iso + $(call box_build,$(1),$(2),$(3),$(4)) + @link='out/$(subst /,-,$@)'; \ + for iso in "$$link"/iso/*.iso; do \ + base=$$(basename "$$iso"); \ + ( cd "$$link/iso" && sha256sum "$$base" ) > "out/$$base.sha256"; \ + echo "out/$$base.sha256"; \ + done +endef + # Instantiate-only counterpart to box_build: same flake expr, but evaluates # `.drvPath` so Nix fully evaluates the config and writes the .drv to the store # WITHOUT realising the (multi-GB) image. Cheap CI validation that the Nix is @@ -105,9 +121,9 @@ endef # ── installer/iso — installer ISO (hosts/_installer-iso); ISO only ──────────── installer/iso: - $(call box_build,_installer-iso,isoImage,,) + $(call box_iso,_installer-iso,isoImage,,) installer/iso/%: - $(call box_build,_installer-iso,isoImage,,$*) + $(call box_iso,_installer-iso,isoImage,,$*) # ── installer/drv — instantiate the installer ISO derivation (no build) ─────── installer/drv: @@ -117,9 +133,9 @@ installer/drv/%: # ── appliance/iso — ephemeral appliance ISO (hosts/_appliance_iso) ─────────── appliance/iso: - $(call box_build,_appliance_iso,isoImage,,) + $(call box_iso,_appliance_iso,isoImage,,) appliance/iso/%: - $(call box_build,_appliance_iso,isoImage,,$*) + $(call box_iso,_appliance_iso,isoImage,,$*) # ── appliance/drv — instantiate the appliance ISO derivation (no build) ─────── appliance/drv: From 579779d0784635140f98fc29be23fd1f5567b1a8 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Sun, 21 Jun 2026 18:34:43 +0000 Subject: [PATCH 12/16] ci: friendly job names + label-gated PR ISO builds - Rename Build ISO matrix jobs to 'Installer ()' / 'Appliance ()' (check shows as 'Build ISO / Installer (...)'); same for the release build jobs. Restructured the matrix to explicit include entries carrying kind/target/label. - On PRs the full ISO for a kind builds only when its label is applied (test-installer-iso / test-appliance-iso); otherwise the job just instantiates the .drv (no image, no artifact). Added the 'labeled' PR trigger so applying a label starts the build. push/main and manual dispatch still always build the full ISO. - Use bare make /iso (native arch per runner) so the out-link path stays out/-iso/iso. --- .github/workflows/build.yml | 118 +++++++++++++++++++++++----------- .github/workflows/release.yml | 42 +++++++----- 2 files changed, 108 insertions(+), 52 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 124de28..422e6a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,15 +9,19 @@ # directory`. So checkout/upload run on the host runner and only `make` runs in # the container. # -# Bare `make ` resolves to the runner's native `builtins.currentSystem` +# `make /iso` resolves to the runner's native `builtins.currentSystem` # (x86_64-linux or aarch64-linux) and produces -# out//iso/coder-box---linux.iso inside the container. The -# container /nix/store is ephemeral, so we copy the ISOs into a host mktemp dir -# bind-mounted at /out and upload from there. +# out/-iso/iso/coder-box---linux.iso (+ a .sha256 sidecar) +# inside the container. The container /nix/store is ephemeral, so we dereference +# the ISO into a host /dist volume and upload from there. # -# Triggers: builds for PRs only when they're ready for review (not on every -# commit/draft push), on merges to main, and manually for any ref via -# workflow_dispatch. Verification build: artifacts are short-lived (1 day). +# Triggers / what gets built: +# * push to main, workflow_dispatch → always build the full ISO. +# * pull_request (ready, non-draft) → build the full ISO for a kind ONLY when +# its label is applied (test-installer-iso / test-appliance-iso); without +# the label we just instantiate the .drv (cheap validation, no image). +# The `labeled` trigger means adding the label kicks off the full build. +# Verification artifacts are short-lived (1 day). name: Build ISO @@ -25,11 +29,11 @@ on: push: branches: [main] pull_request: - # Only ready-for-review PRs: `opened`/`reopened` cover a PR created or - # reopened already non-draft, `ready_for_review` covers a draft promoted to - # ready. Notably absent is `synchronize`, so pushing new commits to an open - # PR does NOT rebuild — the draft guard below also skips draft PRs. - types: [opened, reopened, ready_for_review] + # `opened`/`reopened` cover a PR created/reopened already non-draft, + # `ready_for_review` a draft promoted to ready, and `labeled` so applying a + # test-*-iso label starts the full build. Notably absent is `synchronize`, + # so pushing new commits does NOT rebuild (eval.yml covers per-commit drv). + types: [opened, reopened, ready_for_review, labeled] workflow_dispatch: inputs: ref: @@ -45,19 +49,34 @@ concurrency: jobs: iso: - name: ${{ matrix.target }} (${{ matrix.system }}) - # Skip draft PRs (opened/reopened fire for drafts too); always run for - # push and manual dispatch. + name: ${{ matrix.kind }} (${{ matrix.system }}) + # Skip draft PRs (opened/reopened/labeled fire for drafts too); always run + # for push and manual dispatch. if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: - target: [installer/iso, appliance/iso] include: - - system: x86_64-linux + - kind: Installer + target: installer + label: test-installer-iso + system: x86_64-linux runner: ubuntu-24.04 - - system: aarch64-linux + - kind: Installer + target: installer + label: test-installer-iso + system: aarch64-linux + runner: ubuntu-24.04-arm + - kind: Appliance + target: appliance + label: test-appliance-iso + system: x86_64-linux + runner: ubuntu-24.04 + - kind: Appliance + target: appliance + label: test-appliance-iso + system: aarch64-linux runner: ubuntu-24.04-arm steps: - name: Checkout @@ -67,45 +86,70 @@ jobs: # dispatch to build an arbitrary commit. ref: ${{ github.event.inputs.ref }} - - name: Build ISO (in nixos/nix container) + - name: Decide what to build + id: plan + env: + # full = realise the ISO; otherwise just instantiate the .drv. + # Non-PR events always build the full ISO. PRs require the kind's + # label to be present. + FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, matrix.label) }} + TARGET: ${{ matrix.target }} + run: | + # Bare targets resolve to the runner's native currentSystem (the + # matrix pins the matching native runner per arch). + if [ "$FULL" = "true" ]; then + echo "make=$TARGET/iso" >>"$GITHUB_OUTPUT" + echo "full=true" >>"$GITHUB_OUTPUT" + echo "Building full ISO: make $TARGET/iso" + else + echo "make=$TARGET/drv" >>"$GITHUB_OUTPUT" + echo "full=false" >>"$GITHUB_OUTPUT" + echo "No $TARGET label; instantiating derivation only: make $TARGET/drv" + fi + + - name: Build (in nixos/nix container) id: build env: + MAKE_TARGET: ${{ steps.plan.outputs.make }} + FULL: ${{ steps.plan.outputs.full }} TARGET: ${{ matrix.target }} run: | - # Host /dist volume, bind-mounted into the container, where the build - # drops the finished ISO (the container's /nix/store is ephemeral, so - # the in-tree out/ symlink would dangle on the host — we dereference - # the built ISO into /dist instead). + # Host /dist volume, bind-mounted into the container, where a full + # build drops the finished ISO + checksum (the container's /nix/store + # is ephemeral, so the in-tree out/ symlink would dangle on the host). dist="$(mktemp -d)" echo "dist=$dist" >>"$GITHUB_OUTPUT" - # out/ -> dir>/... e.g. installer/iso -> out/installer-iso - outlink="out/$(echo "$TARGET" | tr '/' '-')/iso" + outlink="out/$TARGET-iso/iso" docker run --rm \ -v "$PWD":/work -w /work \ -v "$dist":/dist \ - -e TARGET \ + -e MAKE_TARGET -e FULL -e outlink="$outlink" \ nixos/nix:latest \ - sh -euc " - # The Makefile builds via 'nix build --impure' and needs flakes + + sh -euc ' + # The Makefile builds via "nix build --impure" and needs flakes + # nix-command, which the nixos/nix image does not enable by default. - export NIX_CONFIG='experimental-features = nix-command flakes' + export NIX_CONFIG="experimental-features = nix-command flakes" # Repo is bind-mounted and owned by a different uid; allow git to # read it so the Makefile can stamp the build rev. git config --global --add safe.directory /work - # gnumake provides 'make'; git lets the Makefile stamp the rev. The + # gnumake provides "make"; git lets the Makefile stamp the rev. The # ISO targets also emit out/.iso.sha256 sidecar checksums. - nix-shell -p gnumake git --run \"make $TARGET\" - # Dereference the build's store symlink into the /dist volume, - # alongside the make-generated checksum sidecar. - cp -L $outlink/*.iso /dist/ - cp out/*.iso.sha256 /dist/ - " + nix-shell -p gnumake git --run "make $MAKE_TARGET" + # Only a full build produces an image; dereference it (+ checksum + # sidecar) into the /dist volume for upload. + if [ "$FULL" = "true" ]; then + cp -L "$outlink"/*.iso /dist/ + cp out/*.iso.sha256 /dist/ + fi + ' ls -lh "$dist" - name: Upload ISO artifact + # drv-only runs produce no image, so nothing to upload. + if: steps.plan.outputs.full == 'true' uses: actions/upload-artifact@v5 with: - name: coder-box-${{ matrix.target == 'installer/iso' && 'installer' || 'appliance' }}-${{ matrix.system }} + name: coder-box-${{ matrix.target }}-${{ matrix.system }} path: ${{ steps.build.outputs.dist }}/*.iso* # Verification build; keep storage cost minimal. retention-days: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b7bfb5..6d15944 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,16 +42,27 @@ concurrency: jobs: build: - name: ${{ matrix.target }} (${{ matrix.system }}) + name: ${{ matrix.kind }} (${{ matrix.system }}) runs-on: ${{ matrix.runner }} strategy: fail-fast: true matrix: - target: [installer/iso, appliance/iso] include: - - system: x86_64-linux + - kind: Installer + target: installer + system: x86_64-linux runner: ubuntu-24.04 - - system: aarch64-linux + - kind: Installer + target: installer + system: aarch64-linux + runner: ubuntu-24.04-arm + - kind: Appliance + target: appliance + system: x86_64-linux + runner: ubuntu-24.04 + - kind: Appliance + target: appliance + system: aarch64-linux runner: ubuntu-24.04-arm steps: - name: Checkout @@ -72,30 +83,31 @@ jobs: # the built ISO into /dist instead). dist="$(mktemp -d)" echo "dist=$dist" >>"$GITHUB_OUTPUT" - outlink="out/$(echo "$TARGET" | tr '/' '-')/iso" + outlink="out/$TARGET-iso/iso" docker run --rm \ -v "$PWD":/work -w /work \ -v "$dist":/dist \ - -e TARGET \ + -e TARGET -e outlink="$outlink" \ nixos/nix:latest \ - sh -euc " - export NIX_CONFIG='experimental-features = nix-command flakes' + sh -euc ' + export NIX_CONFIG="experimental-features = nix-command flakes" git config --global --add safe.directory /work - # gnumake provides 'make'; git lets the Makefile stamp the rev. The + # gnumake provides "make"; git lets the Makefile stamp the rev. The # ISO targets also emit out/.iso.sha256 sidecar checksums so - # release consumers can verify the download. - nix-shell -p gnumake git --run \"make $TARGET\" - # Dereference the build's store symlink into the /dist volume, + # release consumers can verify the download. Bare target → native + # currentSystem (matrix pins the matching native runner). + nix-shell -p gnumake git --run "make $TARGET/iso" + # Dereference the build store symlink into the /dist volume, # alongside the make-generated checksum sidecar. - cp -L $outlink/*.iso /dist/ + cp -L "$outlink"/*.iso /dist/ cp out/*.iso.sha256 /dist/ - " + ' ls -lh "$dist" - name: Upload build artifact uses: actions/upload-artifact@v5 with: - name: coder-box-${{ matrix.target == 'installer/iso' && 'installer' || 'appliance' }}-${{ matrix.system }} + name: coder-box-${{ matrix.target }}-${{ matrix.system }} path: ${{ steps.build.outputs.dist }}/*.iso* if-no-files-found: error From f007c363fca67803d740f18f7c6ff5dd4f856991 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 22 Jun 2026 14:51:38 +0000 Subject: [PATCH 13/16] ci: branch full-vs-drv on label via step if: conditions Replace the runtime 'plan' shell step + single combined build with a job-level FULL env (label check) and two if:-gated steps: 'Build full ISO' (if env.FULL == true) and 'Instantiate ISO derivation' (if env.FULL != true). Upload also keys off env.FULL. --- .github/workflows/build.yml | 81 +++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 422e6a2..3d15cd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,10 +17,11 @@ # # Triggers / what gets built: # * push to main, workflow_dispatch → always build the full ISO. -# * pull_request (ready, non-draft) → build the full ISO for a kind ONLY when -# its label is applied (test-installer-iso / test-appliance-iso); without -# the label we just instantiate the .drv (cheap validation, no image). -# The `labeled` trigger means adding the label kicks off the full build. +# * pull_request (ready, non-draft) → a per-step `if:` on the kind's label +# decides: with test-installer-iso / test-appliance-iso applied, realise +# the full ISO; without it, just instantiate the .drv (cheap validation, +# no image). The `labeled` trigger means adding the label kicks off the +# full build. # Verification artifacts are short-lived (1 day). name: Build ISO @@ -54,6 +55,12 @@ jobs: # for push and manual dispatch. if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ${{ matrix.runner }} + env: + # full = realise the ISO; drv-only otherwise. Non-PR events (push, manual + # dispatch) always build the full ISO; a PR builds a kind's full ISO only + # when its label (test-installer-iso / test-appliance-iso) is applied. + # Step `if:`s below branch on this. + FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, matrix.label) }} strategy: fail-fast: false matrix: @@ -86,44 +93,23 @@ jobs: # dispatch to build an arbitrary commit. ref: ${{ github.event.inputs.ref }} - - name: Decide what to build - id: plan - env: - # full = realise the ISO; otherwise just instantiate the .drv. - # Non-PR events always build the full ISO. PRs require the kind's - # label to be present. - FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, matrix.label) }} - TARGET: ${{ matrix.target }} - run: | - # Bare targets resolve to the runner's native currentSystem (the - # matrix pins the matching native runner per arch). - if [ "$FULL" = "true" ]; then - echo "make=$TARGET/iso" >>"$GITHUB_OUTPUT" - echo "full=true" >>"$GITHUB_OUTPUT" - echo "Building full ISO: make $TARGET/iso" - else - echo "make=$TARGET/drv" >>"$GITHUB_OUTPUT" - echo "full=false" >>"$GITHUB_OUTPUT" - echo "No $TARGET label; instantiating derivation only: make $TARGET/drv" - fi - - - name: Build (in nixos/nix container) + - name: Build full ISO (in nixos/nix container) + # Label present (or non-PR event): realise the full image. + if: env.FULL == 'true' id: build env: - MAKE_TARGET: ${{ steps.plan.outputs.make }} - FULL: ${{ steps.plan.outputs.full }} TARGET: ${{ matrix.target }} run: | - # Host /dist volume, bind-mounted into the container, where a full - # build drops the finished ISO + checksum (the container's /nix/store - # is ephemeral, so the in-tree out/ symlink would dangle on the host). + # Host /dist volume, bind-mounted into the container, where the build + # drops the finished ISO + checksum (the container's /nix/store is + # ephemeral, so the in-tree out/ symlink would dangle on the host). dist="$(mktemp -d)" echo "dist=$dist" >>"$GITHUB_OUTPUT" outlink="out/$TARGET-iso/iso" docker run --rm \ -v "$PWD":/work -w /work \ -v "$dist":/dist \ - -e MAKE_TARGET -e FULL -e outlink="$outlink" \ + -e TARGET -e outlink="$outlink" \ nixos/nix:latest \ sh -euc ' # The Makefile builds via "nix build --impure" and needs flakes + @@ -134,19 +120,34 @@ jobs: git config --global --add safe.directory /work # gnumake provides "make"; git lets the Makefile stamp the rev. The # ISO targets also emit out/.iso.sha256 sidecar checksums. - nix-shell -p gnumake git --run "make $MAKE_TARGET" - # Only a full build produces an image; dereference it (+ checksum - # sidecar) into the /dist volume for upload. - if [ "$FULL" = "true" ]; then - cp -L "$outlink"/*.iso /dist/ - cp out/*.iso.sha256 /dist/ - fi + # Bare target → native currentSystem (matrix pins the runner arch). + nix-shell -p gnumake git --run "make $TARGET/iso" + # Dereference the build store symlink (+ checksum sidecar) into the + # /dist volume for upload. + cp -L "$outlink"/*.iso /dist/ + cp out/*.iso.sha256 /dist/ ' ls -lh "$dist" + - name: Instantiate ISO derivation (in nixos/nix container) + # No label on a PR: just evaluate the .drv (cheap validation, no image). + if: env.FULL != 'true' + env: + TARGET: ${{ matrix.target }} + run: | + docker run --rm \ + -v "$PWD":/work -w /work \ + -e TARGET \ + nixos/nix:latest \ + sh -euc ' + export NIX_CONFIG="experimental-features = nix-command flakes" + git config --global --add safe.directory /work + nix-shell -p gnumake git --run "make $TARGET/drv" + ' + - name: Upload ISO artifact # drv-only runs produce no image, so nothing to upload. - if: steps.plan.outputs.full == 'true' + if: env.FULL == 'true' uses: actions/upload-artifact@v5 with: name: coder-box-${{ matrix.target }}-${{ matrix.system }} From 0d97056c1bcce985600a7eead1eb7e64b3690c2c Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 22 Jun 2026 15:48:45 +0000 Subject: [PATCH 14/16] ci: reorder drv before full build, split iso/sha256 artifacts - Drop '(in nixos/nix container)' from step names. - In Build ISO, run the drv instantiation step before the full ISO build step. - Upload the ISO and its .sha256 as two separate artifacts (was a single *.iso* artifact that zipped both together) in both workflows. --- .github/workflows/build.yml | 45 +++++++++++++++++++++-------------- .github/workflows/release.yml | 13 +++++++--- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d15cd6..d1dfe6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -93,7 +93,23 @@ jobs: # dispatch to build an arbitrary commit. ref: ${{ github.event.inputs.ref }} - - name: Build full ISO (in nixos/nix container) + - name: Instantiate ISO derivation + # No label on a PR: just evaluate the .drv (cheap validation, no image). + if: env.FULL != 'true' + env: + TARGET: ${{ matrix.target }} + run: | + docker run --rm \ + -v "$PWD":/work -w /work \ + -e TARGET \ + nixos/nix:latest \ + sh -euc ' + export NIX_CONFIG="experimental-features = nix-command flakes" + git config --global --add safe.directory /work + nix-shell -p gnumake git --run "make $TARGET/drv" + ' + + - name: Build full ISO # Label present (or non-PR event): realise the full image. if: env.FULL == 'true' id: build @@ -129,29 +145,22 @@ jobs: ' ls -lh "$dist" - - name: Instantiate ISO derivation (in nixos/nix container) - # No label on a PR: just evaluate the .drv (cheap validation, no image). - if: env.FULL != 'true' - env: - TARGET: ${{ matrix.target }} - run: | - docker run --rm \ - -v "$PWD":/work -w /work \ - -e TARGET \ - nixos/nix:latest \ - sh -euc ' - export NIX_CONFIG="experimental-features = nix-command flakes" - git config --global --add safe.directory /work - nix-shell -p gnumake git --run "make $TARGET/drv" - ' - - name: Upload ISO artifact # drv-only runs produce no image, so nothing to upload. if: env.FULL == 'true' uses: actions/upload-artifact@v5 with: name: coder-box-${{ matrix.target }}-${{ matrix.system }} - path: ${{ steps.build.outputs.dist }}/*.iso* + path: ${{ steps.build.outputs.dist }}/*.iso # Verification build; keep storage cost minimal. retention-days: 1 if-no-files-found: error + + - name: Upload ISO checksum artifact + if: env.FULL == 'true' + uses: actions/upload-artifact@v5 + with: + name: coder-box-${{ matrix.target }}-${{ matrix.system }}-sha256 + path: ${{ steps.build.outputs.dist }}/*.iso.sha256 + retention-days: 1 + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d15944..9f96528 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,7 +72,7 @@ jobs: # to release an arbitrary commit. ref: ${{ github.event.inputs.ref }} - - name: Build ISO (in nixos/nix container) + - name: Build ISO id: build env: TARGET: ${{ matrix.target }} @@ -104,11 +104,18 @@ jobs: ' ls -lh "$dist" - - name: Upload build artifact + - name: Upload ISO artifact uses: actions/upload-artifact@v5 with: name: coder-box-${{ matrix.target }}-${{ matrix.system }} - path: ${{ steps.build.outputs.dist }}/*.iso* + path: ${{ steps.build.outputs.dist }}/*.iso + if-no-files-found: error + + - name: Upload ISO checksum artifact + uses: actions/upload-artifact@v5 + with: + name: coder-box-${{ matrix.target }}-${{ matrix.system }}-sha256 + path: ${{ steps.build.outputs.dist }}/*.iso.sha256 if-no-files-found: error release: From 110ca5d78fcd73ac66262a7ed92c24b1fc92c899 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 22 Jun 2026 15:52:41 +0000 Subject: [PATCH 15/16] ci: build both ISOs in one container per arch, vary job title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure Build ISO from a kind×arch matrix into one job per arch that builds BOTH kinds in the SAME container (installer first, appliance later). Each kind is full-built or drv-instantiated independently based on its label. The job title reflects which kinds get a full build: 'Installer + Appliance', 'Installer', 'Appliance', or 'Derivations'. Per-kind ISO + sha256 artifacts uploaded only for full-built kinds. --- .github/workflows/build.yml | 161 ++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 79 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1dfe6c..a5d6675 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,20 +9,21 @@ # directory`. So checkout/upload run on the host runner and only `make` runs in # the container. # -# `make /iso` resolves to the runner's native `builtins.currentSystem` -# (x86_64-linux or aarch64-linux) and produces -# out/-iso/iso/coder-box---linux.iso (+ a .sha256 sidecar) -# inside the container. The container /nix/store is ephemeral, so we dereference -# the ISO into a host /dist volume and upload from there. +# One job per arch builds BOTH kinds in the SAME container (installer first, +# appliance second). `make /iso` resolves to the runner's native +# `builtins.currentSystem` and produces +# out/-iso/iso/coder-box---linux.iso (+ a .sha256 sidecar); +# the container /nix/store is ephemeral, so we dereference the ISOs into a host +# /dist volume and upload from there. # -# Triggers / what gets built: -# * push to main, workflow_dispatch → always build the full ISO. -# * pull_request (ready, non-draft) → a per-step `if:` on the kind's label -# decides: with test-installer-iso / test-appliance-iso applied, realise -# the full ISO; without it, just instantiate the .drv (cheap validation, -# no image). The `labeled` trigger means adding the label kicks off the -# full build. -# Verification artifacts are short-lived (1 day). +# Triggers / what gets built per kind: +# * push to main, workflow_dispatch → always build both full ISOs. +# * pull_request (ready, non-draft) → realise a kind's full ISO only when its +# label (test-installer-iso / test-appliance-iso) is applied; without the +# label that kind is just instantiated (.drv, cheap validation, no image). +# The `labeled` trigger means adding the label kicks off the full build. +# The job title reflects which kinds get a full build. Verification artifacts +# are short-lived (1 day). name: Build ISO @@ -50,40 +51,34 @@ concurrency: jobs: iso: - name: ${{ matrix.kind }} (${{ matrix.system }}) + # Title varies with which kinds get a full build (drv-only kinds aren't + # named): "Installer + Appliance", "Installer", "Appliance", or + # "Derivations" when neither is labelled on a PR. + name: >- + ${{ ( + (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'test-installer-iso')) + && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'test-appliance-iso')) + ) && 'Installer + Appliance' + || (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'test-installer-iso')) && 'Installer' + || (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'test-appliance-iso')) && 'Appliance' + || 'Derivations' }} (${{ matrix.system }}) # Skip draft PRs (opened/reopened/labeled fire for drafts too); always run # for push and manual dispatch. if: github.event_name != 'pull_request' || github.event.pull_request.draft == false runs-on: ${{ matrix.runner }} env: - # full = realise the ISO; drv-only otherwise. Non-PR events (push, manual - # dispatch) always build the full ISO; a PR builds a kind's full ISO only - # when its label (test-installer-iso / test-appliance-iso) is applied. - # Step `if:`s below branch on this. - FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, matrix.label) }} + # Per kind: realise the full ISO, or (drv-only) just instantiate. Non-PR + # events (push, manual dispatch) build both full; a PR builds a kind's + # full ISO only when its label is applied. Steps below branch on these. + INSTALLER_FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'test-installer-iso') }} + APPLIANCE_FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'test-appliance-iso') }} strategy: fail-fast: false matrix: include: - - kind: Installer - target: installer - label: test-installer-iso - system: x86_64-linux + - system: x86_64-linux runner: ubuntu-24.04 - - kind: Installer - target: installer - label: test-installer-iso - system: aarch64-linux - runner: ubuntu-24.04-arm - - kind: Appliance - target: appliance - label: test-appliance-iso - system: x86_64-linux - runner: ubuntu-24.04 - - kind: Appliance - target: appliance - label: test-appliance-iso - system: aarch64-linux + - system: aarch64-linux runner: ubuntu-24.04-arm steps: - name: Checkout @@ -93,39 +88,20 @@ jobs: # dispatch to build an arbitrary commit. ref: ${{ github.event.inputs.ref }} - - name: Instantiate ISO derivation - # No label on a PR: just evaluate the .drv (cheap validation, no image). - if: env.FULL != 'true' - env: - TARGET: ${{ matrix.target }} - run: | - docker run --rm \ - -v "$PWD":/work -w /work \ - -e TARGET \ - nixos/nix:latest \ - sh -euc ' - export NIX_CONFIG="experimental-features = nix-command flakes" - git config --global --add safe.directory /work - nix-shell -p gnumake git --run "make $TARGET/drv" - ' - - - name: Build full ISO - # Label present (or non-PR event): realise the full image. - if: env.FULL == 'true' + - name: Build ISOs id: build - env: - TARGET: ${{ matrix.target }} + # INSTALLER_FULL / APPLIANCE_FULL come from the job-level env above. run: | - # Host /dist volume, bind-mounted into the container, where the build - # drops the finished ISO + checksum (the container's /nix/store is + # Host /dist volume, bind-mounted into the container, where full builds + # drop the finished ISO + checksum (the container's /nix/store is # ephemeral, so the in-tree out/ symlink would dangle on the host). dist="$(mktemp -d)" echo "dist=$dist" >>"$GITHUB_OUTPUT" - outlink="out/$TARGET-iso/iso" + # Both kinds run in ONE container; installer first, appliance later. docker run --rm \ -v "$PWD":/work -w /work \ -v "$dist":/dist \ - -e TARGET -e outlink="$outlink" \ + -e INSTALLER_FULL -e APPLIANCE_FULL \ nixos/nix:latest \ sh -euc ' # The Makefile builds via "nix build --impure" and needs flakes + @@ -134,33 +110,60 @@ jobs: # Repo is bind-mounted and owned by a different uid; allow git to # read it so the Makefile can stamp the build rev. git config --global --add safe.directory /work - # gnumake provides "make"; git lets the Makefile stamp the rev. The - # ISO targets also emit out/.iso.sha256 sidecar checksums. - # Bare target → native currentSystem (matrix pins the runner arch). - nix-shell -p gnumake git --run "make $TARGET/iso" - # Dereference the build store symlink (+ checksum sidecar) into the - # /dist volume for upload. - cp -L "$outlink"/*.iso /dist/ - cp out/*.iso.sha256 /dist/ + + # full build → realise the ISO and copy it (+ its .sha256 sidecar, + # written next to out/) into /dist; otherwise just instantiate the + # derivation. Bare target → native currentSystem (matrix pins the + # runner arch). gnumake provides "make"; git stamps the build rev. + build_kind() { + kind="$1"; full="$2" + if [ "$full" = "true" ]; then + nix-shell -p gnumake git --run "make $kind/iso" + cp -L "out/$kind-iso/iso"/*.iso /dist/ + cp "out/coder-box-$kind"-*.iso.sha256 /dist/ + else + nix-shell -p gnumake git --run "make $kind/drv" + fi + } + + build_kind installer "$INSTALLER_FULL" + build_kind appliance "$APPLIANCE_FULL" ' ls -lh "$dist" - - name: Upload ISO artifact - # drv-only runs produce no image, so nothing to upload. - if: env.FULL == 'true' + - name: Upload installer ISO artifact + if: env.INSTALLER_FULL == 'true' uses: actions/upload-artifact@v5 with: - name: coder-box-${{ matrix.target }}-${{ matrix.system }} - path: ${{ steps.build.outputs.dist }}/*.iso + name: coder-box-installer-${{ matrix.system }} + path: ${{ steps.build.outputs.dist }}/coder-box-installer-*.iso # Verification build; keep storage cost minimal. retention-days: 1 if-no-files-found: error - - name: Upload ISO checksum artifact - if: env.FULL == 'true' + - name: Upload installer checksum artifact + if: env.INSTALLER_FULL == 'true' + uses: actions/upload-artifact@v5 + with: + name: coder-box-installer-${{ matrix.system }}-sha256 + path: ${{ steps.build.outputs.dist }}/coder-box-installer-*.iso.sha256 + retention-days: 1 + if-no-files-found: error + + - name: Upload appliance ISO artifact + if: env.APPLIANCE_FULL == 'true' + uses: actions/upload-artifact@v5 + with: + name: coder-box-appliance-${{ matrix.system }} + path: ${{ steps.build.outputs.dist }}/coder-box-appliance-*.iso + retention-days: 1 + if-no-files-found: error + + - name: Upload appliance checksum artifact + if: env.APPLIANCE_FULL == 'true' uses: actions/upload-artifact@v5 with: - name: coder-box-${{ matrix.target }}-${{ matrix.system }}-sha256 - path: ${{ steps.build.outputs.dist }}/*.iso.sha256 + name: coder-box-appliance-${{ matrix.system }}-sha256 + path: ${{ steps.build.outputs.dist }}/coder-box-appliance-*.iso.sha256 retention-days: 1 if-no-files-found: error From 31e910a17c66e67785592e29455c4d01c20b3285 Mon Sep 17 00:00:00 2001 From: Phorcys Date: Mon, 22 Jun 2026 20:03:19 +0000 Subject: [PATCH 16/16] ci: re-run Build ISO on PR synchronize New commits on a labelled PR were only producing derivations: Build ISO omitted 'synchronize', so pushes never re-triggered it (only eval.yml, which builds drv only, ran). Add synchronize so each commit re-runs the build and re-evaluates labels, rebuilding the full ISO for labelled kinds. --- .github/workflows/build.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5d6675..f62afb0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,10 +32,11 @@ on: branches: [main] pull_request: # `opened`/`reopened` cover a PR created/reopened already non-draft, - # `ready_for_review` a draft promoted to ready, and `labeled` so applying a - # test-*-iso label starts the full build. Notably absent is `synchronize`, - # so pushing new commits does NOT rebuild (eval.yml covers per-commit drv). - types: [opened, reopened, ready_for_review, labeled] + # `ready_for_review` a draft promoted to ready, `labeled` so applying a + # test-*-iso label starts the full build, and `synchronize` so pushing new + # commits re-runs the build — re-evaluating the labels so a labelled kind + # is re-built (not just its derivation) on every commit. + types: [opened, reopened, ready_for_review, labeled, synchronize] workflow_dispatch: inputs: ref: