-
Notifications
You must be signed in to change notification settings - Fork 1
400 lines (375 loc) · 19.1 KB
/
Copy pathtest.yml
File metadata and controls
400 lines (375 loc) · 19.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# 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 }}
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Cache Nix store
uses: nix-community/cache-nix-action@v6
with:
# Key on the lockfile + all Nix sources; restore the most recent
# arch-matching cache otherwise. Cap the saved store so a run can't
# blow past the repo's GitHub Actions cache budget.
primary-key: nix-flake-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock', '**/*.nix') }}
restore-prefixes-first-match: nix-flake-${{ runner.os }}-${{ runner.arch }}-
gc-max-store-size-linux: 5G
- 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 branch on these.
INSTALLER_FULL: ${{ needs.plan.outputs.installer_full }}
APPLIANCE_FULL: ${{ needs.plan.outputs.appliance_full }}
# PR title + number woven into the image's pretty version name (boot-menu
# label + ISO file name) via coderBox.prTitle / coderBox.prNumber. Set
# through `env:` (not inlined into a run script) so an arbitrary title
# can't break the shell, and empty for non-PR events so tag/main builds
# keep their plain names.
CODER_BOX_PR_TITLE: ${{ github.event.pull_request.title }}
CODER_BOX_PR_NUMBER: ${{ github.event.pull_request.number }}
# Branch name for the boot-screen label's "<short-sha>@<branch>" stamp. A
# PR checkout is a detached HEAD (so the Makefile's `git rev-parse
# --abbrev-ref HEAD` would say "HEAD"); github.head_ref is the real source
# branch on pull_request events and empty otherwise (the Makefile then
# falls back to the local branch name).
CODER_BOX_BRANCH: ${{ github.head_ref }}
strategy:
fail-fast: false
matrix:
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 }}
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Cache Nix store
uses: nix-community/cache-nix-action@v6
with:
# ISO closures are large; cap the saved store so a build can't blow
# past the repo's GitHub Actions cache budget (10G total).
primary-key: nix-images-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock', '**/*.nix') }}
restore-prefixes-first-match: nix-images-${{ runner.os }}-${{ runner.arch }}-
gc-max-store-size-linux: 8G
# Per-kind plan (full ISO vs drv only) is in the job name; the run summary
# below also records it. INSTALLER_FULL / APPLIANCE_FULL come from the
# job-level env above.
- name: Build images
id: build
env:
# These are verification images, not shipped artifacts, so trade ISO
# size for build speed: a low squashfs compression level is far faster
# than the nixpkgs default (zstd level 19), which otherwise dominates
# the build — and the rev baked into /etc forces that recompress on
# every commit regardless of caching. Releases keep the slow default.
ISO_COMPRESSION: zstd -Xcompression-level 3
run: |
# Record the per-kind plan in the run summary for quick scanning.
plan() { [ "$1" = "true" ] && echo "full ISO" || echo "derivation only"; }
{
echo "### Build plan (${{ matrix.system }})"
echo "- installer: $(plan "$INSTALLER_FULL")"
echo "- appliance: $(plan "$APPLIANCE_FULL")"
} >>"$GITHUB_STEP_SUMMARY"
# Nix is on the host now, so make/git (preinstalled on the runner)
# build straight into the host /nix/store — no container. A full build
# → make <kind>/iso, then dereference the image + its .sha256 sidecar
# (colocated in out/<kind>-iso/iso) into a real dir for upload; a
# drv-only kind just instantiates. Bare target → native currentSystem.
dist="$(mktemp -d)"
echo "dist=$dist" >>"$GITHUB_OUTPUT"
# Nix is on the host (make/git preinstalled on the runner) so the
# build goes straight into the host /nix/store — no container. The
# job-level CODER_BOX_PR_TITLE / CODER_BOX_PR_NUMBER env is inherited
# by make directly (read under --impure for the pretty version name).
build_kind() {
kind="$1"; full="$2"
if [ "$full" = "true" ]; then
make "$kind/iso"
cp -L "out/$kind-iso/iso"/* "$dist/"
else
make "$kind/drv"
fi
}
build_kind installer "$INSTALLER_FULL"
build_kind appliance "$APPLIANCE_FULL"
ls -lh "$dist"
# 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 });
}