Files
omarchy-iso/.github/workflows/nightly-build.yml
T
Emir Beganovic bb1ad28887 CI: the four actions at their latest releases, and Dependabot weekly
actions/checkout             v5.0.0 -> v7.0.1
  actions/upload-artifact      v6.0.0 -> v7.0.2
  actions/download-artifact    v4.3.0 -> v8.0.2
  docker/setup-buildx-action   v4.3.0 -> v4.4.1

Each stays pinned to a commit, with the version in its comment.

From the release notes, what the new majors change and why none of it
touches these workflows: download-artifact v5 moved the path of an
artifact fetched by ID, and ours is fetched by name; v8 fails on a
checksum mismatch where v4 said nothing, which is what a publish job
wants. checkout v7 refuses a fork's code under pull_request_target and
workflow_run, and the one workflow_run here checks nothing out. The
Node 24 releases need runner 2.327.1; ours run 2.337.0.

Pinned actions do not update themselves, so .github/dependabot.yml asks
for one grouped pull request a week.
2026-10-09 22:14:54 +02:00

289 lines
13 KiB
YAML

name: Nightly ISO Build
# Every night, for two channels: installs an ISO unattended and boots what it
# installed (the install test), turns that installed disk into a VM image, and
# publishes it with a latest.json saying what is in it.
#
# edge the ISO is built here, from the edge channel: its packages, its
# mirrors and the edge builds of Omarchy (omarchy-dev)
# stable the ISO is the last release: omacom/omarchy's latest release names
# the version, iso.omarchy.org has the ISO, its signature is checked.
# Done once for each release, skipped on the nights after.
#
# https://nightly.omarchy.org/<channel>/latest.json
# https://nightly.omarchy.org/edge/<version>/omarchy-edge-<version>.iso
# https://nightly.omarchy.org/edge/<version>/omarchy-edge-<version>.qcow2
# https://nightly.omarchy.org/edge/<version>/unpacked/ kernel, initramfs, vm-boot
# https://nightly.omarchy.org/stable/<version>/<run>/omarchy-<version>.qcow2
# https://nightly.omarchy.org/stable/<version>/<run>/unpacked/
#
# Nothing is published unless the ISO installed and the image passed its
# checks. A version's files are written once and never replaced: the CDN keeps
# them for a year. Only latest.json changes, and it is written last.
#
# No pull_request trigger: this workflow holds the key to the bucket, so it
# never runs for a fork. To try a change, run it by hand on a branch; that
# builds and checks everything and publishes nothing.
on:
schedule:
- cron: "1 0 * * *" # Daily at 00:01 UTC
workflow_dispatch:
inputs:
publish:
description: "Publish to nightly.omarchy.org (only from quattro)"
type: boolean
default: false
permissions:
contents: read
concurrency:
group: nightly-${{ github.ref }}
cancel-in-progress: false
env:
SITE: https://nightly.omarchy.org
jobs:
# Which channels this run makes, under which version, and where each goes:
# test/vm-image/nightly-plan decides, from what is fetched here.
plan:
name: Plan
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
channels: ${{ steps.plan.outputs.channels }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- id: plan
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
release=$(gh api repos/omacom/omarchy/releases/latest --jq .tag_name)
published=$(curl -fsL "$SITE/stable/latest.json?run=$GITHUB_RUN_ID" | jq -r .version || true)
channels=$(./test/vm-image/nightly-plan "$(date -u +%Y.%m.%d)" "${{ github.run_number }}" "${{ github.run_attempt }}" \
"$GITHUB_EVENT_NAME" "$release" "$published")
echo "channels=$channels" | tee -a "$GITHUB_OUTPUT"
build:
name: ${{ matrix.channel }} ${{ matrix.version }}
needs: plan
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.channels) }}
runs-on: namespace-profile-linux-amd64-16x32
timeout-minutes: 90
env:
CHANNEL: ${{ matrix.channel }}
VERSION: ${{ matrix.version }}
RELEASE: ${{ matrix.release }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Runner
run: |
uname -srm
echo "$(nproc) CPUs, $(free -h | awk 'NR==2 {print $2}') memory"
grep -m1 'model name' /proc/cpuinfo || true
echo "LSMs: $(cat /sys/kernel/security/lsm 2>/dev/null || echo unreadable)"
ls -l /dev/kvm
- name: Check disk space
run: |
df -h /
avail_gb=$(df -BG --output=avail / | tail -1 | tr -dc 0-9)
[ "$avail_gb" -ge 50 ] || { echo "::error::only ${avail_gb} GB free, the build, the install test and the image need ~50 GB"; exit 1; }
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
if: matrix.channel == 'edge'
# No package cache, on purpose: every package the ISO is made of is
# fetched from the mirrors again each night, so one that has gone
# missing there fails this build instead of being served from a cache.
- name: Build the edge ISO
if: matrix.channel == 'edge'
run: NO_BOOT_OFFER=1 ./bin/omarchy-iso-make --edge
# Good only if the signature is the release key's, the one in this repo.
- name: Download the released ISO and check its signature
if: matrix.channel == 'stable'
run: |
set -euo pipefail
mkdir -p release
iso=omarchy-$VERSION.iso
curl -fSL --retry 3 -o "release/$iso" "https://iso.omarchy.org/$iso"
curl -fsSL --retry 3 -o "release/$iso.sig" "https://iso.omarchy.org/$iso.sig"
home=$(mktemp -d)
gpg --homedir "$home" --quiet --batch --import builder/omarchy.gpg
key=$(gpg --homedir "$home" --batch --with-colons --list-keys | awk -F: '$1 == "fpr" { print $10; exit }')
gpg --homedir "$home" --batch --status-fd 1 --verify "release/$iso.sig" "release/$iso" 2>/dev/null |
grep -q "^\[GNUPG:\] VALIDSIG $key " || { echo "::error::$iso is not signed by $key"; exit 1; }
echo "$iso is signed by $key"
rm -rf "$home" "release/$iso.sig"
# The same install test a pull request gets (install-test.yml), then the
# image: the installed disk that test leaves behind, sealed and checked.
- name: Install unattended, boot the installed system, seal it
run: |
sudo chmod 0666 /dev/kvm
docker run --rm --device /dev/kvm -v "$PWD:/w" -w /w \
-e OMARCHY_INTEGRATION_DISK_CACHE=none \
archlinux:latest bash -euo pipefail -c '
# pacman 7 fails every sync on a kernel without Landlock, which
# these runners have; builder/build-iso.sh does the same.
if ! grep -qw landlock /sys/kernel/security/lsm 2>/dev/null; then
printf "#!/bin/sh\nexec /usr/bin/pacman --disable-sandbox \"\$@\"\n" >/usr/local/bin/pacman
chmod 0755 /usr/local/bin/pacman
fi
# The tools come from our own Arch mirror first, the public ones
# after it. A mirror that drops one file must not fail the test:
# try again.
sed -i "1i Server = https://mirror.omarchy.org/\$repo/os/\$arch" /etc/pacman.d/mirrorlist
for attempt in 1 2 3; do
pacman -Syu --noconfirm --needed qemu-system-x86 qemu-hw-display-virtio-gpu qemu-hw-display-virtio-vga qemu-img edk2-ovmf socat \
imagemagick tesseract tesseract-data-eng mtools dosfstools jq openssh openssl python curl binutils >/dev/null && break
[ "$attempt" -lt 3 ] || { echo "::error::the test container could not install its tools"; exit 1; }
echo "pacman failed, attempt $attempt of 3; trying again in 15 s"; sleep 15
done
# test/integration installs its tools through Omarchy'"'"'s helper; they are installed above.
printf "#!/bin/sh\nexit 0\n" >/usr/local/bin/omarchy-pkg-add
chmod 0755 /usr/local/bin/omarchy-pkg-add
qemu-system-x86_64 --version | head -1
./test/integration release/*.iso boot --no-preview --memory 8192 --timeout 1800
base=$(ls -d test-runs/*-integration)
./test/vm-image/vm-seal "$base" sealed
./test/vm-image/vm-verify sealed | tee vm-verify.txt
./test/vm-image/vm-boot sealed "pacman -Qq omarchy omarchy-dev 2>/dev/null | xargs pacman -Q; pacman -Qo /usr/lib/modules/\$(uname -r)/pkgbase; uname -r" >vm-packages.txt
cat vm-packages.txt
'
# The container ran as root; what it wrote belongs to root until here.
- name: Hand the results to the runner's user
if: always()
run: |
mkdir -p test-runs sealed
sudo chown -R "$(id -u):$(id -g)" test-runs sealed release vm-*.txt 2>/dev/null || true
- name: Lay out the release and describe it
env:
ISO_COMMIT: ${{ github.sha }}
run: |
set -euo pipefail
./test/vm-image/nightly-layout "$CHANNEL" "$VERSION" "$RELEASE" release/*.iso sealed vm-packages.txt out
dir=out/$RELEASE
{
echo "## Nightly $CHANNEL $VERSION"
echo
echo "| File | Size |"
echo "| --- | ---: |"
(cd "$dir" && find . -type f -printf '| `%P` | %s |\n' | sort) | awk -F'|' '{ printf "|%s| %.0f MiB |\n", $2, $3 / 1048576 }'
echo
echo '```'
cat vm-verify.txt
echo '```'
} >>"$GITHUB_STEP_SUMMARY"
- name: Keep the release for the publish job
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: nightly-${{ matrix.channel }}-${{ github.run_id }}
path: out/
compression-level: 0
retention-days: 2
if-no-files-found: error
- name: Keep the install test's logs
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: nightly-install-test-${{ matrix.channel }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
test-runs/*/omarchy-install-timing.json
test-runs/*/omarchy-install.log
test-runs/*/first-boot-systemd-analyze.txt
test-runs/*/runs/**/*.png
test-runs/*/runs/**/*.txt
test-runs/*/runs/**/*.json
test-runs/*/runs/**/*.log
vm-verify.txt
vm-packages.txt
if-no-files-found: ignore
retention-days: 14
# The only job that holds the bucket's key, and it builds nothing: it copies
# what the job above made, checks what the CDN then serves, and only then
# points latest.json at it.
# One channel failing does not hold back the other: each is published if
# its own build left a release behind.
publish:
name: Publish ${{ matrix.channel }}
needs: [plan, build]
if: ${{ !cancelled() && github.ref == 'refs/heads/quattro' && (github.event_name == 'schedule' || inputs.publish) }}
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.channels) }}
runs-on: ubuntu-24.04
timeout-minutes: 45
environment: nightly
env:
CHANNEL: ${{ matrix.channel }}
VERSION: ${{ matrix.version }}
RELEASE: ${{ matrix.release }}
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
BUCKET: omarchy-nightly
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: nightly-${{ matrix.channel }}-${{ github.run_id }}
path: out
- name: rclone
run: |
[ -n "$RCLONE_CONFIG_R2_ACCESS_KEY_ID" ] || { echo "::error::the nightly environment has no bucket key"; exit 1; }
sudo apt-get update -qq && sudo apt-get install -y -qq rclone >/dev/null
rclone version | head -1
# --immutable: a name that exists with other bytes is an error, not an
# overwrite.
- name: Upload this version
run: |
rclone copy "out/$RELEASE" "r2:$BUCKET/$RELEASE" \
--immutable --s3-no-check-bucket --transfers 8 --s3-upload-concurrency 8 --s3-chunk-size 64M --stats 30s --stats-one-line
# Fetched the way a user fetches them. It proves what is served, and it
# is the first request the CDN sees for each file.
- name: Check what the CDN serves
run: |
set -euo pipefail
dir=out/$RELEASE
check() { # url, expected sha256
got=$(curl -fsSL --retry 3 "$1" | sha256sum | cut -d" " -f1)
[ "$got" = "$2" ] || { echo "::error::$1 serves $got, expected $2"; exit 1; }
echo "ok $1"
}
# Every file of the version, the checksum files too.
(cd "$dir" && find . -type f -printf '%P\n' | sort) >files.txt
[ -s files.txt ] || { echo "::error::nothing to check under $dir"; exit 1; }
while read -r file; do
check "$SITE/$RELEASE/$file" "$(sha256sum "$dir/$file" | cut -d" " -f1)"
done <files.txt
echo "$(wc -l <files.txt) files published and checked"
- name: Point latest.json at it
run: |
rclone copyto "out/$CHANNEL/latest.json" "r2:$BUCKET/$CHANNEL/latest.json" --s3-no-check-bucket
served=$(curl -fsSL "$SITE/$CHANNEL/latest.json?run=$GITHUB_RUN_ID" | jq -r .version)
[ "$served" = "$VERSION" ] || { echo "::error::latest.json serves version $served, expected $VERSION"; exit 1; }
echo "Published [$VERSION]($SITE/$CHANNEL/latest.json)" >>"$GITHUB_STEP_SUMMARY"