Repository navigation
Expand file tree
/
Copy pathjustfile
More file actions
556 lines (521 loc) · 27.8 KB
/
Copy pathjustfile
File metadata and controls
556 lines (521 loc) · 27.8 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
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# iso-builder task runner.
#
# Every recipe here is what a CI step used to inline in
# .github/workflows/ci.yml; the workflow keeps only the thin wrappers
# (checkout, setup, artifact transfer, deploy, anything bearing secrets).
# Recipes read their configuration from the environment the workflow step
# sets (SLUG, BENCHMARK, QUALIFY, ...), exactly as the inline scripts did, so a
# step's `env:` block remains the one place its inputs are visible.
#
# Written for the `just` that Ubuntu's apt ships (1.21): shebang recipes and
# plain recipes only, nothing newer. native/justfile is a separate, unrelated
# file for the desktop app's packaging.
set shell := ["bash", "-euo", "pipefail", "-c"]
default:
@just --list
# ── CI cleanup and reaping ─────────────────────────────────────────────────
# Job-scoped cleanup for an `if: always()` step. Removes only what THIS run
# created (its own ISOs and scratch under $RUNNER_TEMP, its run-scoped
# Playwright profile, its luks-verify-<SLUG> harness dirs, Playwright output,
# and the QEMU/swtpm/Chromium processes started against those paths). Safe to
# run when nothing exists; refuses (exit 1) rather than delete a path outside
# its roots. SLUG may be a comma-separated list; DRY_RUN=1 only prints.
# Remove this job's own ISOs, scratch, harness output and processes.
ci-cleanup:
python3 scripts/ci-cleanup.py
# Cancel stuck runs and delete old runs/expired artifacts through `gh api`.
# Env: STALE_QUEUED_HOURS (6), STALE_RUNNING_HOURS (12), KEEP_RUN_DAYS (14),
# DRY_RUN=1 prints only. Needs GH_TOKEN with actions: write and
# GITHUB_REPOSITORY (or REPO). Never touches the newest run per
# workflow+branch, its own run, or young default-branch pushes.
# Cancel stuck runs; delete old runs and expired artifacts.
ci-reap-stale:
python3 scripts/ci-reap-stale.py
# ── Local checks ───────────────────────────────────────────────────────────
# Python contract/unit tests (what qualification-contract runs in CI).
test:
python3 -m pytest -q tests
# Node unit tests that run offline (builder config, guided catalog, relay shim).
test-node:
node --test tests/*.test.cjs worker/*.test.js
# Everything that runs offline.
check: test test-node
# Import a CatalogSnapshot made by tunaOS scripts/generate-catalog-snapshot.py
# into the web, native and app.js copies, and regenerate the guided catalog.
# Import a tunaOS-generated catalog snapshot.
catalog-sync snapshot:
python3 scripts/sync-catalog-snapshot.py "{{ snapshot }}"
# ── wasm job ───────────────────────────────────────────────────────────────
# Expand native/go.mod's abbreviated tacklebox pin to a full commit SHA via
# the API (actions/checkout cannot resolve a 12-char pseudo-version). Writes
# `commit=<sha>` to $GITHUB_OUTPUT when set. Needs GH_TOKEN.
# Resolve the pinned tacklebox commit SHA.
wasm-resolve-pin:
#!/usr/bin/env bash
set -euo pipefail
cd native
ver=$(go list -m -f '{{{{.Version}}' github.com/tuna-os/tacklebox)
short=${ver##*-}
full=$(gh api "repos/tuna-os/tacklebox/commits/${short}" --jq .sha)
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "commit=${full}" >> "$GITHUB_OUTPUT"
fi
echo "pinned tacklebox ${ver} -> ${short} -> ${full}"
# Build tbox.wasm from the checked-out tacklebox (.tacklebox), take
# wasm_exec.js from the same toolchain so the shim matches the compiler, and
# report both.
# Build tbox.wasm and wasm_exec.js from the pinned tacklebox.
wasm-build:
#!/usr/bin/env bash
set -euo pipefail
root="{{justfile_directory()}}"
(cd .tacklebox && GOOS=js GOARCH=wasm go build -o "${root}/app/public/tbox.wasm" ./cmd/tbwasm)
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" app/public/wasm_exec.js
ls -l app/public/tbox.wasm app/public/wasm_exec.js
# ── e2e + full-matrix: shared browser setup ────────────────────────────────
# CACHE_HIT=true skips the exact Playwright browser download. Only the full
# Ubuntu 24.04 Actions runner uses its preinstalled libraries; actual Chromium
# launch and close must succeed there. Other contexts install the OS libraries.
# Install Playwright deps and chromium.
e2e-install:
#!/usr/bin/env bash
set -euo pipefail
cd e2e
if [ -n "${PREINSTALLED_BROWSER_DEPS:-}" ]; then
if [ "$PREINSTALLED_BROWSER_DEPS" != "true" ] || [ "${GITHUB_ACTIONS:-}" != "true" ] || \
[ "${RUNNER_OS:-}" != "Linux" ] || [ "${RUNNER_ARCH:-}" != "X64" ] || [ "${ImageOS:-}" != "ubuntu24" ]; then
echo "PREINSTALLED_BROWSER_DEPS requires the Ubuntu 24.04 x64 GitHub Actions runner" >&2
exit 1
fi
fi
npm ci
if [ "${PREINSTALLED_BROWSER_DEPS:-}" = "true" ]; then
if [ "${CACHE_HIT:-}" != "true" ]; then npx playwright install chromium; fi
node -e 'const { chromium } = require("@playwright/test"); (async () => { const browser = await chromium.launch({ args: ["--disable-dev-shm-usage"] }); console.log("Chromium runtime ready:", browser.version()); await browser.close(); })().catch(error => { console.error(error); process.exitCode = 1; });'
elif [ "${CACHE_HIT:-}" != "true" ]; then
npx playwright install --with-deps chromium
else
npx playwright install-deps chromium
fi
# The merge gate: page load + inspect only, no ISO written. The browser
# profile root is scoped to this run so ci-cleanup can own it.
# Run the browser merge gate (no @full).
e2e-gate:
#!/usr/bin/env bash
set -euo pipefail
if [ -n "${RUNNER_TEMP:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
export TBOX_E2E_PROFILE_ROOT="${TBOX_E2E_PROFILE_ROOT:-${RUNNER_TEMP}/pw-profile-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}}"
fi
cd e2e
npx playwright test --grep-invert @full
# ── full-matrix ────────────────────────────────────────────────────────────
# Resolve e2e/editions.json into the matrix, honouring the dispatch inputs
# (ONLY, RUN_FULL, KEEP_ISO, ...). Output is `editions=`/`count=` lines:
# just full-plan >> "$GITHUB_OUTPUT" (not --quiet: it drops recipe stdout)
# Resolve the full-matrix plan from the dispatch inputs.
full-plan:
python3 scripts/plan-full-matrix.py
# Build the edition in the browser (@full). Benchmark mode (BENCHMARK=true)
# takes three cold samples. Pair mode (PAIR=true) takes three cold/warm pairs:
# samples 1, 3, 5 are cold (fresh profile, kept on disk) and 2, 4, 6 are warm
# (the preceding cold sample's profile and OPFS layer cache, identical inputs;
# the spec fails a warm sample that fetches any complete layer blob).
# Pair measurements are build-only; they do not retain an ISO or start a guest.
# JSON receipts and attempt outcomes are preserved outside Playwright's output
# directory between invocations, including the first failing attempt.
# Build the edition's ISO in the browser (@full).
full-build:
#!/usr/bin/env bash
set -euo pipefail
if [ -n "${GITHUB_RUN_ID:-}" ]; then
export TBOX_E2E_PROFILE_ROOT="${TBOX_E2E_PROFILE_ROOT:-${RUNNER_TEMP}/pw-profile-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT:-1}}"
fi
cd e2e
# The spec writes one QualificationEvidence v1 file per sample; the
# evidence step merges benchmark median/range or the pair summary.
if [ "${PAIR:-}" = "true" ] && [ "${BENCHMARK:-}" = "true" ]; then
echo "PAIR=true and BENCHMARK=true are mutually exclusive" >&2; exit 1
fi
if [ "${PAIR:-}" = "true" ]; then SAMPLES="1 2 3 4 5 6"
elif [ "${BENCHMARK:-}" = "true" ]; then SAMPLES="1 2 3"
else SAMPLES="1"; fi
last="${SAMPLES##* }"
evidence_dir="${RUNNER_TEMP}/browser-samples-${SLUG}-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
mkdir "${evidence_dir}"
export TBOX_E2E_PAIR_STATE="${evidence_dir}/pair-inputs.json"
restore_evidence() {
mkdir -p test-results
shopt -s nullglob
saved=("${evidence_dir}/"*.json)
if [ "${#saved[@]}" -gt 0 ]; then cp "${saved[@]}" test-results/; fi
}
trap restore_evidence EXIT
for s in $SAMPLES; do
echo "=== @full sample ${s} ==="
if [ "${PAIR:-}" = "true" ]; then
pair=$(( (s + 1) / 2 ))
if [ $(( s % 2 )) -eq 1 ]; then mode=cold keep=1; else mode=warm keep=; fi
echo "=== pair ${pair}: ${mode} ==="
export TBOX_E2E_PAIR="${pair}" TBOX_E2E_CACHE_MODE="${mode}" TBOX_E2E_PROFILE_KEEP="${keep}"
fi
copy="${RUNNER_TEMP}/${SLUG}-s${s}.iso"
if [ "${PAIR:-}" = "true" ]; then copy=""; fi
status=0
TBOX_E2E_SAMPLE="${s}" \
TBOX_E2E_ISO_COPY="${copy}" \
TBOX_E2E_EVIDENCE="test-results/qual-evidence-${SLUG}-s${s}.json" \
npx playwright test --grep @full --timeout=4500000 || status=$?
printf '{"sample":%s,"exitCode":%s}\n' "${s}" "${status}" > "${evidence_dir}/build-attempt-${SLUG}-s${s}.json"
shopt -s nullglob
receipts=(test-results/*"qual-evidence-${SLUG}-s${s}.json")
if [ "${#receipts[@]}" -gt 0 ]; then cp "${receipts[@]}" "${evidence_dir}/"; fi
if [ "${status}" -ne 0 ]; then exit "${status}"; fi
if [ "${s}" != "${last}" ] && [ -n "${copy}" ]; then rm -f "${copy}"; fi
done
if [ "${PAIR:-}" != "true" ]; then
cp "${RUNNER_TEMP}/${SLUG}-s${last}.iso" "${RUNNER_TEMP}/${SLUG}.iso"
fi
# Install the built ISO under QEMU with LUKS and verify the installed system
# reboots (tunaOS scripts/iso-e2e.sh from the pinned tunaos-e2e checkout).
# Env: SLUG, QUALIFY, TBOX_E2E_IMAGE.
# Install and boot the built ISO under QEMU (LUKS or strict GUI).
full-guest-verify:
#!/usr/bin/env bash
set -euo pipefail
ISO="${RUNNER_TEMP}/${SLUG}.iso"
if [ ! -f "$ISO" ]; then
echo "::error::ISO file $ISO was not produced by Playwright build"
exit 1
fi
echo "Installing QEMU, OVMF, swtpm dependencies..."
sudo apt-get update -qq
# imagemagick (magick/convert/compare), tesseract-ocr and sshpass are
# what the strict walkthrough measures with. Without them it reports
# stddev 0.0 for every frame and skips screen detection, so a guest that
# painted fine is reported blank (run 40235407606). jq is used by the
# harness's evidence printing.
sudo apt-get install -y --no-install-recommends \
qemu-system-x86 qemu-utils ovmf swtpm swtpm-tools socat netpbm python3-pil \
imagemagick tesseract-ocr sshpass jq
echo "Enabling KVM permissions..."
# A self-hosted pod gets /dev/kvm from a device plugin and has no udev.
if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
echo "/dev/kvm already accessible; skipping udev rule"
else
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | \
sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm || true
fi
# Qualification targets run Task 11's strict offline-GUI
# contract; upstream pipeline references keep the LUKS check.
# sudo sanitizes the environment; the VAR=val prefix carries the
# image ref through to the harness.
if [ "${QUALIFY}" = "true" ]; then
echo "Running strict offline-GUI verification..."
sudo env -u TBOX_E2E_IMAGE "${GITHUB_WORKSPACE}/tunaos-e2e/scripts/iso-e2e.sh" "$ISO" --strict-offline-gui \
--output "luks-verify-${SLUG}" \
--timeout 900
else
echo "Running LUKS installation and reboot verification..."
sudo TBOX_E2E_IMAGE="$TBOX_E2E_IMAGE" "${GITHUB_WORKSPACE}/tunaos-e2e/scripts/iso-e2e.sh" "$ISO" --luks \
--output "luks-verify-${SLUG}" \
--timeout 900
fi
echo "Guest verification completed successfully!"
# Split the built ISO into hash-pinned chunks for the GitHub artifact route
# and publish the browser-measured ISO hash as `iso_sha256=` in $GITHUB_OUTPUT.
# Env: SLUG (plus the GITHUB_* defaults). No secrets.
# Split the ISO into hash-pinned chunks for the artifact route.
iso-artifact-split:
#!/usr/bin/env bash
set -euo pipefail
python3 scripts/retain-iso-artifact.py split \
--iso "$RUNNER_TEMP/${SLUG}.iso" \
--measurements "e2e/test-results/build-measurements-qual-evidence-${SLUG}-s1.json" \
--out "$RUNNER_TEMP/iso-artifact" --slug "$SLUG" \
--diagnostic-unqualified "${DIAGNOSTIC_UNQUALIFIED:-false}" \
--repository "$GITHUB_REPOSITORY" --run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" --sha "$GITHUB_SHA" \
> "$RUNNER_TEMP/iso-artifact-manifest.json"
mkdir -p e2e/test-results/iso-artifact-manifest
cp "$RUNNER_TEMP/iso-artifact/manifest.json" e2e/test-results/iso-artifact-manifest/manifest.json
# Expected hash comes from the browser measurement, not from the manifest under test.
iso_sha=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["iso"]["sha256"])' "e2e/test-results/build-measurements-qual-evidence-${SLUG}-s1.json")
echo "iso_sha256=${iso_sha}" >> "$GITHUB_OUTPUT"
# Free the local copy first so the round trip reads GitHub's bytes, not ours.
# The strict route also drops the original ISO. Env: SLUG, STRICT_ROUNDTRIP_TEST.
# Drop the local ISO chunks before the artifact round trip.
iso-artifact-drop-local:
#!/usr/bin/env bash
set -euo pipefail
rm -rf "$RUNNER_TEMP/iso-artifact" "$RUNNER_TEMP/iso-roundtrip"
if [ "$STRICT_ROUNDTRIP_TEST" = "true" ]; then
rm -f "$RUNNER_TEMP/${SLUG}.iso"
fi
# Re-verify the downloaded artifact chunk by chunk and as a whole against the
# browser measurement, and write the step summary. Env: SLUG, ARTIFACT,
# ISO_SHA256, ARTIFACT_ID, STRICT_ROUNDTRIP_TEST, RETAIN_ISO_ARTIFACT_DAYS.
# Verify the downloaded ISO artifact against the browser measurement.
iso-artifact-verify:
#!/usr/bin/env bash
set -euo pipefail
output_args=()
if [ "$STRICT_ROUNDTRIP_TEST" = "true" ]; then
output_args=(--output "$RUNNER_TEMP/${SLUG}-roundtrip.iso")
fi
python3 scripts/retain-iso-artifact.py verify --dir "$RUNNER_TEMP/iso-roundtrip" \
--diagnostic-unqualified "${DIAGNOSTIC_UNQUALIFIED:-false}" \
--expected-sha256 "$ISO_SHA256" "${output_args[@]}" > "$RUNNER_TEMP/iso-roundtrip-result.json"
{
echo "### ISO retained as GitHub artifact (observed, not qualified)"
echo ""
echo "- artifact: \`${ARTIFACT}\` (id ${ARTIFACT_ID}), retention ${RETAIN_ISO_ARTIFACT_DAYS} day(s)"
echo "- ISO sha256: \`${ISO_SHA256}\`"
echo "- downloaded again and re-verified chunk by chunk and as a whole against the browser measurement"
echo '```json'
cat "$RUNNER_TEMP/iso-roundtrip-result.json"
echo '```'
echo "Fetch: \`gh run download ${GITHUB_RUN_ID} -R ${GITHUB_REPOSITORY} -n ${ARTIFACT}\` then \`python3 scripts/retain-iso-artifact.py verify --dir <dir> --diagnostic-unqualified ${DIAGNOSTIC_UNQUALIFIED:-false} --expected-sha256 <browser-measured hash> --output ${SLUG}.iso\`."
} >> "$GITHUB_STEP_SUMMARY"
# Derive the strict-guest inputs from the round-tripped media receipts and
# export them to later steps through $GITHUB_ENV. Env: SLUG, EDITION_REF,
# PLATFORM, ARTIFACT_NAME, ARTIFACT_ID, REPOSITORY, RUN_ID, RUN_ATTEMPT,
# SOURCE_SHA.
# Derive strict-guest inputs from the round-tripped media receipts.
full-strict-inputs:
#!/usr/bin/env bash
set -euo pipefail
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
xorriso squashfs-tools qemu-system-x86 qemu-utils ovmf swtpm swtpm-tools socat \
netpbm python3-pil imagemagick tesseract-ocr sshpass jq
IFS=: read -r variant flavor <<< "$EDITION_REF"
iso="$RUNNER_TEMP/${SLUG}-roundtrip.iso"
python3 scripts/prepare-browser-selftest.py \
--iso "$iso" \
--measurements "e2e/test-results/build-measurements-qual-evidence-${SLUG}-s1.json" \
--browser-evidence "e2e/test-results/qual-evidence-${SLUG}-s1.json" \
--manifest "$RUNNER_TEMP/iso-roundtrip/manifest.json" \
--payload-extractor "$GITHUB_WORKSPACE/tunaos-e2e-current/scripts/extract-iso-payloads.py" \
--repository "$REPOSITORY" --run-id "$RUN_ID" --run-attempt "$RUN_ATTEMPT" \
--source-sha "$SOURCE_SHA" --artifact-id "$ARTIFACT_ID" --artifact-name "$ARTIFACT_NAME" \
--variant "$variant" --flavor "$flavor" --slug "$SLUG" --platform "$PLATFORM" \
--install-mode "$STRICT_INSTALL_MODE" --diagnostic-unqualified "$DIAGNOSTIC_UNQUALIFIED" \
--out-env "$RUNNER_TEMP/same-byte-inputs.env" \
--out-diagnostics "e2e/test-results/same-byte-receipt-diagnostics-${SLUG}.log" \
--out-binding "e2e/test-results/same-byte-guest-test-binding-${SLUG}.json"
cat "$RUNNER_TEMP/same-byte-inputs.env" >> "$GITHUB_ENV"
{
echo "### Same-byte strict guest test inputs (${STRICT_INSTALL_MODE}, diagnostic-unqualified=${DIAGNOSTIC_UNQUALIFIED})"
echo "Media receipts, browser measurements, run/attempt and artifact id matched. This does not qualify the target."
cat "e2e/test-results/same-byte-guest-test-binding-${SLUG}.json"
} >> "$GITHUB_STEP_SUMMARY"
# Run the exact retained ISO through the strict harness: one install mode
# (plain, or diagnostic luks; observed, never full qualification) or both legs
# (STRICT_INSTALL_MODE=both: plain then LUKS on the same bytes in one harness
# run, the input to build-qualification-receipt). The TUNAOS_QUAL_* identities
# come from $GITHUB_ENV, written by full-strict-inputs.
# Env: SLUG, STRICT_INSTALL_MODE, DIAGNOSTIC_UNQUALIFIED.
# Run the exact retained ISO under the current strict harness.
full-guest-verify-strict:
#!/usr/bin/env bash
set -euo pipefail
STRICT_INSTALL_MODE="${STRICT_INSTALL_MODE:-plain}"
DIAGNOSTIC_UNQUALIFIED="${DIAGNOSTIC_UNQUALIFIED:-false}"
case "$STRICT_INSTALL_MODE" in
plain|luks) HARNESS_MODES="$STRICT_INSTALL_MODE" ;;
both) HARNESS_MODES="plain,luks" ;;
*) echo "::error::STRICT_INSTALL_MODE must be plain, luks or both (got '$STRICT_INSTALL_MODE')"; exit 1 ;;
esac
# The harness binds monitor/vnc/swtpm/serial sockets under OUTPUT; the
# longest is strict-plain-warm-serial.sock, and sun_path holds 107 bytes.
# No suffix on the observed dir: yellowfin-gnome-nvidia with "-obs" reached
# 110 bytes on the runner workspace. The non-strict guest step never runs
# in the same job, and ci-cleanup already owns luks-verify-<slug>.
OUT="luks-verify-${SLUG}"
if [ "$DIAGNOSTIC_UNQUALIFIED" = "true" ]; then OUT="luks-verify-${SLUG}-diag"; fi
SOCK="$PWD/$OUT/strict-plain-warm-serial.sock"
if [ "${#SOCK}" -ge 108 ]; then
echo "::error::Harness socket path is ${#SOCK} bytes, over the 107-byte sun_path limit: $SOCK"
exit 1
fi
ISO="$RUNNER_TEMP/${SLUG}-roundtrip.iso"
if [ ! -f "$ISO" ]; then
echo "::error::Verified round-trip ISO is missing: $ISO"
exit 1
fi
# A self-hosted pod gets /dev/kvm from a device plugin and has no udev.
if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
echo "/dev/kvm already accessible; skipping udev rule"
else
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | \
sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm || true
fi
if [ "$DIAGNOSTIC_UNQUALIFIED" = "true" ]; then
if [ "$STRICT_INSTALL_MODE" != "luks" ]; then
echo "::error::Diagnostic strict testing requires LUKS"
exit 1
fi
STRICT_INSTALL_MODE="$STRICT_INSTALL_MODE" DIAGNOSTIC_UNQUALIFIED="$DIAGNOSTIC_UNQUALIFIED" \
python3 scripts/browser-strict-diagnostic.py \
--harness-root "$GITHUB_WORKSPACE/tunaos-e2e-current" \
--iso "$ISO" --binding "e2e/test-results/same-byte-guest-test-binding-${SLUG}.json" \
--output "$OUT"
else
sudo env -u TBOX_E2E_IMAGE -u FISHERMAN_OVERRIDE -u TUNAOS_INSTALLED_BOOT_DIAGNOSTIC \
VARIANT="$VARIANT" FLAVOR="$FLAVOR" \
TUNAOS_QUAL_EXECUTION_KIND="$TUNAOS_QUAL_EXECUTION_KIND" \
STRICT_PUBLISHED_MEDIA="$STRICT_PUBLISHED_MEDIA" \
TUNAOS_QUAL_SOURCE_DIGEST="$TUNAOS_QUAL_SOURCE_DIGEST" \
TUNAOS_QUAL_SOURCE_CONFIG_DIGEST="${TUNAOS_QUAL_SOURCE_CONFIG_DIGEST:-}" \
TUNAOS_QUAL_SOURCE_REF="$TUNAOS_QUAL_SOURCE_REF" \
TUNAOS_QUAL_OVERLAY_DIGEST="$TUNAOS_QUAL_OVERLAY_DIGEST" \
TUNAOS_QUAL_INSTALLER_DIGEST="$TUNAOS_QUAL_INSTALLER_DIGEST" \
TUNAOS_QUAL_ENGINE_DIGEST="$TUNAOS_QUAL_ENGINE_DIGEST" \
TUNAOS_QUAL_MEDIA_DIGEST="$TUNAOS_QUAL_MEDIA_DIGEST" \
TUNAOS_QUAL_PLATFORM="$TUNAOS_QUAL_PLATFORM" \
TUNAOS_QUAL_CPU_BASELINE="$TUNAOS_QUAL_CPU_BASELINE" \
TUNAOS_QUAL_HARDWARE_SCOPE="$TUNAOS_QUAL_HARDWARE_SCOPE" \
TUNAOS_QUAL_BROWSER_NAME="$TUNAOS_QUAL_BROWSER_NAME" \
TUNAOS_QUAL_BROWSER_VERSION="$TUNAOS_QUAL_BROWSER_VERSION" \
TUNAOS_QUAL_BROWSER_MEMORY_GIB="$TUNAOS_QUAL_BROWSER_MEMORY_GIB" \
TUNAOS_QUAL_BROWSER_FREE_DISK_GIB="$TUNAOS_QUAL_BROWSER_FREE_DISK_GIB" \
"$GITHUB_WORKSPACE/tunaos-e2e-current/scripts/iso-e2e.sh" "$ISO" --strict-offline-gui="$HARNESS_MODES" \
--output "$OUT" --timeout 900
fi
if [ "$STRICT_INSTALL_MODE" = "both" ]; then
scope="plain and LUKS legs passed; a qualification receipt is built only on refs/heads/main"
else
scope="not full qualification"
fi
{
echo "### Same-byte strict guest test completed (${STRICT_INSTALL_MODE}, diagnostic-unqualified=${DIAGNOSTIC_UNQUALIFIED}; ${scope})"
echo "- ISO: \`${TUNAOS_QUAL_MEDIA_DIGEST}\`"
echo "- Harness modes \`${HARNESS_MODES}\`; observations and media-bound identities are attached."
} >> "$GITHUB_STEP_SUMMARY"
# Build the BrowserIsoQualificationReceipt/v1 from a passed both-mode strict
# run (harness fragments + same-byte binding + this run's GITHUB_* identity),
# writing canonical receipt.json for attestation. Refuses anything but a
# non-diagnostic both-mode run on refs/heads/main. Env: SLUG,
# STRICT_INSTALL_MODE, DIAGNOSTIC_UNQUALIFIED (plus the GITHUB_* defaults).
# Build the canonical qualification receipt from a passed both-mode run.
full-qualification-receipt:
#!/usr/bin/env bash
set -euo pipefail
mkdir -p "$RUNNER_TEMP/qualification-receipt"
python3 scripts/build-qualification-receipt.py \
--harness-output "luks-verify-${SLUG}" \
--binding "e2e/test-results/same-byte-guest-test-binding-${SLUG}.json" \
--workflow .github/workflows/ci.yml \
--harness-checkout "$GITHUB_WORKSPACE/tunaos-e2e-current" \
--slug "$SLUG" \
--install-mode "$STRICT_INSTALL_MODE" --diagnostic-unqualified "$DIAGNOSTIC_UNQUALIFIED" \
--out "$RUNNER_TEMP/qualification-receipt/receipt.json"
digest="sha256:$(sha256sum "$RUNNER_TEMP/qualification-receipt/receipt.json" | cut -d' ' -f1)"
{
echo "### Browser ISO qualification receipt (${SLUG})"
echo "- receipt digest: \`${digest}\`"
echo '```json'
python3 -m json.tool "$RUNNER_TEMP/qualification-receipt/receipt.json"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# Put the attestation bundle beside the receipt as bundle.jsonl and
# authenticate it exactly as the consumer will, including that the signing
# certificate names this run attempt and source commit. Env: BUNDLE_PATH
# (attest-build-provenance's bundle-path output), GH_TOKEN.
# Verify the receipt's attestation bundle as the consumer will.
full-qualification-receipt-verify:
#!/usr/bin/env bash
set -euo pipefail
dir="$RUNNER_TEMP/qualification-receipt"
if [ -z "${BUNDLE_PATH:-}" ] || [ ! -s "$BUNDLE_PATH" ]; then
echo "::error::attestation bundle is missing: '${BUNDLE_PATH:-}'"
exit 1
fi
cp "$BUNDLE_PATH" "$dir/bundle.jsonl"
gh attestation verify "$dir/receipt.json" --repo tuna-os/iso-builder \
--signer-workflow tuna-os/iso-builder/.github/workflows/ci.yml \
--source-ref refs/heads/main --bundle "$dir/bundle.jsonl" \
--format json > "$dir/verify.json"
python3 - "$dir/verify.json" <<'PY'
import json, os, sys
results = json.load(open(sys.argv[1]))
run = f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}/attempts/{os.environ['GITHUB_RUN_ATTEMPT']}"
certs = [r["verificationResult"]["signature"]["certificate"] for r in results]
if not certs or any(c.get("runInvocationURI") != run or c.get("sourceRepositoryDigest") != os.environ["GITHUB_SHA"] for c in certs):
sys.exit(f"attestation certificate does not name {run} at {os.environ['GITHUB_SHA']}: {certs}")
print(f"receipt attestation verified for {run}")
PY
# Validate a harness-only change against the frames a failed strict run already
# captured, in seconds instead of a ~3 h dispatch. Download the diag artifact
# (`gh run download <run> -n full-<slug>-diag`), check out the harness at the
# pin ci.yml names, then: just strict-replay <dir> [--text-only] [--expect progress]
# Needs tesseract-ocr and imagemagick unless --text-only. Env: HARNESS_ROOT
# (default tunaos-e2e-current).
# Replay recorded strict walkthrough frames through the current harness driver.
strict-replay DIR *ARGS:
python3 scripts/replay-strict-frames.py "{{DIR}}" --harness-root "${HARNESS_ROOT:-tunaos-e2e-current}" {{ARGS}}
# The guest harness runs under sudo, so its output starts root-owned. Label
# the strict receipt first, then hand the tree back to the runner user so
# upload-artifact can read it. Env: SLUG, STRICT_ROUNDTRIP_TEST, DIAGNOSTIC_UNQUALIFIED.
# Label diagnostic output when requested and hand harness output back to the runner user.
full-label-output:
#!/usr/bin/env bash
set -euo pipefail
if [ "$STRICT_ROUNDTRIP_TEST" = "true" ] && [ "$DIAGNOSTIC_UNQUALIFIED" = "true" ]; then
sudo python3 scripts/finalize-diagnostic-output.py \
"luks-verify-${SLUG}-diag"
fi
shopt -s nullglob
dirs=(luks-verify-*/)
if [ "${#dirs[@]}" -gt 0 ]; then
sudo chown -R "$(id -u):$(id -g)" "${dirs[@]}"
fi
# Validate the browser QualificationEvidence v1 files (and merge the three
# benchmark samples, or summarize the three cold/warm pairs). Env: SLUG,
# BENCHMARK, PAIR.
# Validate browser qualification evidence; merge benchmark samples.
full-validate-evidence:
#!/usr/bin/env bash
set -euo pipefail
cd e2e
python3 ../scripts/validate-qualification-evidence.py
# Fail closed unless this host can give the strict guest real GL: KVM, a DRM
# render node, and a QEMU with virtio-vga-gl and egl-headless. Without all of
# them iso-e2e.sh silently falls back to software rendering and Pantheon,
# niri, KDE, XFCE and COSMIC reproduce the black-installer failure of the
# hosted runners. Env: TBOX_E2E_RENDERNODE (default /dev/dri/renderD128).
gpu-host-preflight:
#!/usr/bin/env bash
set -euo pipefail
node="${TBOX_E2E_RENDERNODE:-/dev/dri/renderD128}"
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends qemu-system-x86 qemu-utils
# Ubuntu ships the GL display and device modules separately since QEMU 8.
if apt-cache show qemu-system-modules-opengl >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends qemu-system-modules-opengl
fi
fail=0
for dev in /dev/kvm "$node"; do
if sudo test -c "$dev"; then
echo "ok - $dev present"
else
echo "::error::$dev is missing on this runner"; fail=1
fi
done
if qemu-system-x86_64 -device help 2>/dev/null | grep -q virtio-vga-gl; then
echo "ok - qemu offers virtio-vga-gl"
else
echo "::error::qemu-system-x86_64 has no virtio-vga-gl device"; fail=1
fi
if qemu-system-x86_64 -display help 2>/dev/null | grep -q egl-headless; then
echo "ok - qemu offers egl-headless"
else
echo "::error::qemu-system-x86_64 has no egl-headless display"; fail=1
fi
df -h / "$RUNNER_TEMP" || true
free -g || true
exit "$fail"