Skip to content

refactor: use nixos-facter module from nixpkgs instead of flake input #109

refactor: use nixos-facter module from nixpkgs instead of flake input

refactor: use nixos-facter module from nixpkgs instead of flake input #109

Workflow file for this run

# Test Nix — evaluates the flake and builds the installer AND appliance ISOs
# for every supported architecture.
#
# A `flake` job runs `nix flake check` (cheap, builds nothing) so Nix typos /
# bad references / type errors surface fast; the matrix build jobs then realise
# (or, drv-only, just instantiate) the images.
#
# Nix is installed natively on each runner (DeterminateSystems/nix-installer-
# action) rather than run from 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 entirely and, crucially,
# lets us cache the /nix/store across runs (nix-community/cache-nix-action,
# backed by the GitHub Actions cache). Each arch builds on its own native
# runner, so `make <kind>/iso` resolves to the runner's native
# `builtins.currentSystem`.
#
# Triggers / what gets built per kind:
# * push to main → drv-only: just instantiate each kind's
# derivation (cheap validation, no image). A full ISO build on every main
# commit is expensive and unnecessary; releases (tags) and manual runs
# still produce real images.
# * workflow_dispatch → always build both full ISOs (manual,
# on-demand image build).
# * pull_request → realise a kind's full ISO only when the
# PR is ready-for-review (non-draft) AND its label (test-installer-iso /
# test-appliance-iso) is applied; otherwise (draft, or no label) that kind
# is just instantiated (.drv, cheap validation, no image). The `labeled`
# trigger means adding the label kicks off the full build.
# A tiny `plan` job computes the per-kind plan once and a short title fragment
# so the build job's name stays readable. The build job always runs (drafts just
# do derivations). Verification artifacts are short-lived (1 day).
name: Test Nix
on:
push:
branches: [main]
pull_request:
# `opened`/`reopened` cover a PR created/reopened already non-draft,
# `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:
description: "Git ref/commit to build (defaults to the selected branch)"
required: false
type: string
# 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 }}-${{ github.event.inputs.ref }}
cancel-in-progress: true
env:
# Force plain, greppable Nix output in CI logs. Nix's default animated
# multi-line progress bar renders as unreadable ANSI redraw noise in the
# GitHub Actions log viewer; `--log-format raw` prints one line per event and
# `--print-build-logs` streams the actual builder output. The Makefile passes
# $(NIX_OUTPUT_FLAGS) to every nix invocation (build / eval / flake check).
NIX_OUTPUT_FLAGS: --log-format raw --print-build-logs
jobs:
# Flake evaluation — cheap, builds nothing. `nix flake check --no-build
# --all-systems` evaluates every flake output (nixosConfigurations, packages,
# …) for all declared systems (x86_64 + aarch64), catching typos / bad
# references / type errors in seconds. The per-kind ISO derivations are
# instantiated separately by the `Images` job below (its drv-only path), so
# this covers the flake outputs that path doesn't touch.
flake:
name: Flake eval
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.ref }}
# 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:
cache-key-prefix: nix-flake
gc-max-store-size: 5G
- name: Flake check
run: make check
# Tiny pre-job that decides, per kind, whether to build the full ISO or just
# instantiate the derivation, and assembles a short human title for the build
# job. Doing this here (rather than inline in the build job's `name:`) keeps
# that name a SHORT expression — `Build ${{ needs.plan.outputs.kinds }}
# (${{ matrix.system }})` — so the raw "Matrix:" preview / a skipped job shows
# something readable instead of a wall of inlined label checks.
plan:
name: Plan image targets
runs-on: ubuntu-latest
outputs:
# "true"/"false" per kind: realise the full ISO, or (drv-only) instantiate.
installer_full: ${{ steps.plan.outputs.installer_full }}
appliance_full: ${{ steps.plan.outputs.appliance_full }}
# Human title fragment, e.g. "installer & appliance ISO" or
# "installer ISO & appliance DRV".
kinds: ${{ steps.plan.outputs.kinds }}
steps:
- id: plan
# Manual dispatch builds both full; push to main is drv-only; a PR
# builds a kind's full ISO only when it is ready-for-review (non-draft)
# AND its label is applied. push / draft PRs / unlabelled kinds →
# drv-only.
env:
INSTALLER_FULL: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'test-installer-iso')) }}
APPLIANCE_FULL: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false && contains(github.event.pull_request.labels.*.name, 'test-appliance-iso')) }}
run: |
isuf=$([ "$INSTALLER_FULL" = "true" ] && echo ISO || echo DRV)
asuf=$([ "$APPLIANCE_FULL" = "true" ] && echo ISO || echo DRV)
# Always join the two kinds with "&". Collapse to a shared suffix when
# they match, otherwise spell each out.
if [ "$isuf" = "$asuf" ]; then
kinds="installer & appliance $isuf"
else
kinds="installer $isuf & appliance $asuf"
fi
{
echo "installer_full=$INSTALLER_FULL"
echo "appliance_full=$APPLIANCE_FULL"
echo "kinds=$kinds"
} >>"$GITHUB_OUTPUT"
# Job key is "Images" so the matrix shows as "Matrix: Images". `needs: plan`
# also means these matrix jobs are skipped if the plan job fails.
Images:
needs: plan
# Short, readable name — the per-kind plan is computed by the `plan` job
# above. e.g. "Build installer & appliance ISO (x86_64-linux)" or
# "Build installer ISO & appliance DRV (aarch64-linux)".
name: Build ${{ needs.plan.outputs.kinds }} (${{ matrix.system }})
runs-on: ${{ matrix.runner }}
env:
# 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 "<short-sha>@<branch>" 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 }}
strategy:
fail-fast: false
matrix:
include:
- system: x86_64-linux
runner: ubuntu-24.04
- system: aarch64-linux
runner: ubuntu-24.04-arm
steps:
- name: Checkout
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 }}
# 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
# 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
run: |
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"
# 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
# these per-arch files and renders them into a single sticky PR comment.
# Skipped when nothing built full (drv-only runs produce no ISOs).
- name: Record ISO sizes
if: env.INSTALLER_FULL == 'true' || env.APPLIANCE_FULL == 'true'
run: |
dist="${{ steps.build.outputs.dist }}"
meta="$dist/iso-sizes-${{ matrix.system }}.tsv"
: >"$meta"
for f in "$dist"/*.iso; do
[ -e "$f" ] || continue
printf '%s\t%s\t%s\n' "${{ matrix.system }}" "$(basename "$f")" "$(stat -c %s "$f")" >>"$meta"
done
cat "$meta"
# Per-arch ISO size metadata, kept tiny + short-lived. Named per system so
# the table job can merge both arches' files into one dir without clashing.
- name: Upload ISO size metadata
if: env.INSTALLER_FULL == 'true' || env.APPLIANCE_FULL == 'true'
uses: actions/upload-artifact@v6
with:
name: iso-meta-${{ matrix.system }}
path: ${{ steps.build.outputs.dist }}/iso-sizes-${{ matrix.system }}.tsv
retention-days: 1
if-no-files-found: ignore
# One artifact per kind, each bundling that kind's ISO with its .sha256
# sidecar (a single upload-artifact step uploads its matched files
# CONCURRENTLY, so the multi-GB ISO and its checksum go up together).
# Installer and appliance stay in SEPARATE artifacts so each kind can be
# downloaded on its own. Each step runs only when its kind built full.
- name: Upload installer ISO artifact
if: env.INSTALLER_FULL == 'true'
uses: actions/upload-artifact@v6
with:
name: coder-box-installer-${{ matrix.system }}
path: |
${{ steps.build.outputs.dist }}/coder-box-installer-*.iso
${{ steps.build.outputs.dist }}/coder-box-installer-*.iso.sha256
# Verification build; keep storage cost minimal.
retention-days: 1
if-no-files-found: error
- name: Upload appliance ISO artifact
if: env.APPLIANCE_FULL == 'true'
uses: actions/upload-artifact@v6
with:
name: coder-box-appliance-${{ matrix.system }}
path: |
${{ steps.build.outputs.dist }}/coder-box-appliance-*.iso
${{ steps.build.outputs.dist }}/coder-box-appliance-*.iso.sha256
retention-days: 1
if-no-files-found: error
# Render / refresh a single sticky PR comment with a table of the ISO build
# artifacts (kind × arch): exact size + a download link. Runs after the matrix
# builds, only on pull_request and only when at least one kind built a full
# ISO (drv-only validation runs produce no artifacts to list). Every rebuild
# updates the SAME comment — matched by the hidden marker — instead of posting
# a new one each time.
iso-table:
name: ISO artifact table
needs: [plan, Images]
if: >-
github.event_name == 'pull_request' &&
(needs.plan.outputs.installer_full == 'true' || needs.plan.outputs.appliance_full == 'true')
runs-on: ubuntu-24.04
permissions:
pull-requests: write
steps:
# Pull both arches' per-arch size TSVs into one dir (filenames are
# system-suffixed, so merge-multiple can't clash).
- name: Download ISO size metadata
uses: actions/download-artifact@v7
with:
path: meta
pattern: iso-meta-*
merge-multiple: true
- name: Upsert ISO artifact table comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// 1. Collect exact ISO sizes from the per-arch TSV rows.
const rows = [];
const dir = 'meta';
const files = fs.existsSync(dir) ? fs.readdirSync(dir) : [];
for (const f of files) {
if (!f.startsWith('iso-sizes-') || !f.endsWith('.tsv')) continue;
const text = fs.readFileSync(path.join(dir, f), 'utf8');
for (const line of text.split('\n')) {
if (!line.trim()) continue;
const [system, file, bytes] = line.split('\t');
rows.push({ system, file, bytes: Number(bytes) });
}
}
// 2. Map each ISO artifact name -> its browser download URL.
const { owner, repo } = context.repo;
const runId = context.runId;
const arts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts,
{ owner, repo, run_id: runId, per_page: 100 },
);
const urlFor = (name) => {
const a = arts.find((x) => x.name === name);
return a
? `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${a.id}`
: null;
};
// Small presentation helpers.
const fmtSize = (b) => {
if (!Number.isFinite(b) || b <= 0) return '—';
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0, n = b;
while (n >= 1024 && i < u.length - 1) { n /= 1024; i += 1; }
return `${n.toFixed(i >= 2 ? 2 : 0)} ${u[i]}`;
};
const prettyArch = (s) => s.replace(/-linux$/, '');
const kindOf = (file) => file.includes('-installer-')
? { slug: 'installer', label: 'Installer' }
: { slug: 'appliance', label: 'Appliance' };
// 3. Build the table, sorted by kind (installer before appliance)
// then arch for a stable layout.
const kindRank = (file) => (file.includes('-installer-') ? 0 : 1);
rows.sort((a, b) =>
kindRank(a.file) - kindRank(b.file) || a.system.localeCompare(b.system));
let body = '## 📀 ISO build artifacts\n\n';
if (rows.length === 0) {
body += '_No ISO artifacts were produced in this run._\n';
} else {
body += '| Kind | Arch | Size | Download |\n|:--|:--|--:|:--:|\n';
for (const r of rows) {
const k = kindOf(r.file);
const url = urlFor(`coder-box-${k.slug}-${r.system}`);
const dl = url ? `[⬇️ \`${r.file}\`](${url})` : '—';
body += `| ${k.label} | ${prettyArch(r.system)} | ${fmtSize(r.bytes)} | ${dl} |\n`;
}
}
const sha = (context.payload.pull_request?.head?.sha || context.sha).slice(0, 7);
body += `\n<sub>↻ Updated for \`${sha}\` · `
+ `[run #${context.runNumber}](https://github.com/${owner}/${repo}/actions/runs/${runId}) · `
+ `artifacts expire in ~1 day · sign in to GitHub to download.</sub>\n`;
// 4. Upsert the sticky comment, matched by this hidden marker.
const MARKER = '<!-- iso-build-table -->';
body += `\n${MARKER}`;
const issue_number = context.payload.pull_request.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number, per_page: 100,
});
const existing = comments.find((c) => c.body && c.body.includes(MARKER));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}