17 files changed
+1218
-49
No files matched your search
@@ -21,13 +21,16 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
install:
|
||||
runs-on: ubuntu-24.04
|
||||
# Our own branches run on Namespace. A pull request from a fork stays on
|
||||
# GitHub's runner: it is somebody else's code, and it has no business on
|
||||
# our account's machines. No cache volume either way: what one pull
|
||||
# request leaves in a cache the next run would trust.
|
||||
runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-24.04' || 'namespace-profile-linux-amd64-16x32' }}
|
||||
timeout-minutes: 75
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
# The hosted runner has far more disk than the build needs (145 GB,
|
||||
# ~86 GB free); fail early and loudly if that ever changes.
|
||||
# Fail early and loudly if the runner ever has less disk than this needs.
|
||||
- name: Check disk space
|
||||
run: |
|
||||
df -h /
|
||||
@@ -48,8 +51,22 @@ jobs:
|
||||
docker run --rm --device /dev/kvm -v "$PWD:/w" -w /w \
|
||||
-e OMARCHY_INTEGRATION_DISK_CACHE=none \
|
||||
archlinux:latest bash -euo pipefail -c '
|
||||
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 >/dev/null
|
||||
# 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 >/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
|
||||
|
||||
@@ -1,35 +1,288 @@
|
||||
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: # Allow manual triggering
|
||||
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:
|
||||
build-iso:
|
||||
runs-on: ubuntu-latest
|
||||
# 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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- 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"
|
||||
|
||||
- name: Free Disk Space
|
||||
uses: jlumbroso/free-disk-space@v1.3.1
|
||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: Build ISO
|
||||
run: NO_BOOT_OFFER=1 ./bin/omarchy-iso-make
|
||||
- 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: Delete previously uploaded ISO artifact
|
||||
uses: philips-labs/action-delete-artifacts@v1.0.0
|
||||
- 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@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
workflow: nightly-build.yml
|
||||
continue-on-error: true
|
||||
name: nightly-${{ matrix.channel }}-${{ github.run_id }}
|
||||
path: out/
|
||||
compression-level: 0
|
||||
retention-days: 2
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload newly built ISO artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Keep the install test's logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: omarchy-iso-${{ github.run_number }}
|
||||
path: ./release/*.iso
|
||||
retention-days: 1
|
||||
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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
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"
|
||||
@@ -46,7 +46,14 @@ jobs:
|
||||
sleep 1
|
||||
done
|
||||
echo "systemd: $state"
|
||||
pacman -Syu --noconfirm --needed python parted >/dev/null
|
||||
# From our own Arch mirror first, and a second and third try: a
|
||||
# mirror that drops one file must not fail the tests.
|
||||
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 python parted diffutils jq >/dev/null && break
|
||||
[ "$attempt" -lt 3 ] || { echo "::error::the test container could not install its tools"; exit 1; }
|
||||
sleep 15
|
||||
done
|
||||
useradd -m tester
|
||||
cp -r /src /home/tester/omarchy-iso
|
||||
chown -R tester /home/tester/omarchy-iso
|
||||
|
||||
@@ -1,35 +1,152 @@
|
||||
# Omarchy ISO
|
||||
|
||||
The Omarchy ISO is the only supported way to install Omarchy. It ships the Omarchy Configurator, installs Arch Linux, installs the Omarchy packages from the bundled mirror, runs target system setup in the chroot, creates the user, and runs `omarchy-setup-user` for that user.
|
||||
[](https://github.com/omacom/omarchy-iso/actions/workflows/nightly-build.yml?query=branch%3Aquattro+event%3Aschedule)
|
||||
|
||||
## Downloading the latest ISO
|
||||
The Omarchy ISO is the only supported way to install Omarchy. It includes the Omarchy Configurator and installs Arch Linux and the Omarchy packages from the bundled mirror. It then runs target system setup in the chroot, creates the user, and runs `omarchy-provision-user` for that user.
|
||||
|
||||
See the ISO link on [omarchy.org](https://omarchy.org).
|
||||
## Get Omarchy
|
||||
|
||||
Every published ISO has a `.sha256` beside it at the same URL. Download both into the same directory and check the ISO before writing it to a USB stick:
|
||||
1. [Download the ISO](https://omarchy.org).
|
||||
2. Write it to a USB stick. Use [balenaEtcher](https://etcher.balena.io/) on Mac or Windows, or [caligula](https://github.com/ifd3f/caligula) on Linux.
|
||||
3. Turn off Secure Boot and/or TPM in the BIOS.
|
||||
4. Boot from the stick.
|
||||
5. Answer the configuration questions and select a drive.
|
||||
|
||||
The install takes under a minute on the fastest machines and up to 5 minutes on an older computer.
|
||||
|
||||
### Verify the download
|
||||
|
||||
Verify the ISO before you write it to the stick. Otherwise, a corrupted download causes a pacman "invalid or corrupted package" error several minutes into the install.
|
||||
|
||||
Every release ISO has a `.sha256` file and a `.sig` file next to it. Run these commands to download and verify the ISO of the latest release of [omacom/omarchy](https://github.com/omacom/omarchy/releases):
|
||||
|
||||
```bash
|
||||
sha256sum -c omarchy-3.0.iso.sha256
|
||||
version=$(curl -fsSL https://api.github.com/repos/omacom/omarchy/releases/latest | jq -r .tag_name)
|
||||
version=${version#v}
|
||||
curl -fLO "https://iso.omarchy.org/omarchy-$version.iso"
|
||||
curl -fsSLO "https://iso.omarchy.org/omarchy-$version.iso.sha256"
|
||||
sha256sum -c "omarchy-$version.iso.sha256"
|
||||
```
|
||||
|
||||
Corruption anywhere in the ISO is worth catching before the write, and corruption in the bundled package mirror is worth catching most: the mirror lives inside the ISO and the installer reads it straight off the medium, so those bytes surface minutes into the install as a pacman "invalid or corrupted package" error rather than as anything that names the download. Corruption elsewhere is louder and earlier — it stops the medium booting or mounting. There is a `.sig` beside the ISO too for anyone who wants to verify it against the Omarchy signing key.
|
||||
Use the `.sig` file to verify the ISO against the Omarchy signing key.
|
||||
|
||||
## Creating the ISO
|
||||
### More in the manual
|
||||
|
||||
Run `./bin/omarchy-iso-make`; output goes into `./release`. By default the ISO uses the Omarchy packages and tracks the `quattro` branch, from the stable mirror. Pass `--edge` to use `omarchy-dev` and `omarchy-settings-dev` from the edge mirror.
|
||||
- [Getting started](https://omarchy.org/manual/getting-started/): the install step by step, installing for another owner, and installing without encryption.
|
||||
- [Dual-boot install](https://omarchy.org/manual/dual-boot-install/): install Omarchy in the free space of a drive, beside Windows or another OS.
|
||||
- [Unattended installs](https://omarchy.org/manual/unattended-installs/): let the ISO install itself from a configuration on a second drive, with no keyboard and no wizard.
|
||||
|
||||
For local development, build the ISO from sibling checkouts:
|
||||
# For maintainers
|
||||
|
||||
The rest of this file covers building, testing and publishing the ISO.
|
||||
|
||||
## What is published, and where
|
||||
|
||||
| What | Where | When |
|
||||
| --- | --- | --- |
|
||||
| Release ISO | `https://iso.omarchy.org/omarchy-<version>.iso` | Each release |
|
||||
| Edge ISO | `https://nightly.omarchy.org/edge/<version>/omarchy-edge-<version>.iso` | Every night |
|
||||
| VM image of edge | `https://nightly.omarchy.org/edge/<version>/omarchy-edge-<version>.qcow2` | Every night |
|
||||
| VM image of the latest release | `https://nightly.omarchy.org/stable/<version>/<run>/omarchy-<version>.qcow2` | Once per release |
|
||||
| `latest.json` | `https://nightly.omarchy.org/edge/latest.json` and `.../stable/latest.json` | With each nightly build |
|
||||
|
||||
Every ISO and every VM image has a `.sha256` file next to it. The release ISO also has a `.sig` file. Each VM image has an `unpacked/` folder next to it.
|
||||
|
||||
On nightly.omarchy.org, a published file is never replaced. Only `latest.json` changes. It is written last, after every file of the version has been fetched back through the CDN and compared.
|
||||
|
||||
## The nightly build
|
||||
|
||||
`.github/workflows/nightly-build.yml` runs every night for two channels:
|
||||
|
||||
- `edge`: builds the ISO from the edge channel: the edge packages and mirrors, and the edge builds of Omarchy (`omarchy-dev`).
|
||||
- `stable`: uses the latest release. The latest release of omacom/omarchy determines the version. The ISO comes from iso.omarchy.org and is used only if its signature matches the release key. A scheduled run skips a release that is already published. A manual run builds it again.
|
||||
|
||||
Each ISO goes through the same install test that runs for pull requests: an unattended install, then a boot of the installed system. `test/vm-image/vm-seal` then turns the installed disk into a VM image, and `test/vm-image/vm-verify` boots that image and checks it. Nothing is published unless every check passes.
|
||||
|
||||
A separate job publishes. It is the only job with the bucket's key, and it runs only on `quattro`. To try a change, run the workflow manually on a branch. That run builds and checks everything and publishes nothing.
|
||||
|
||||
`test/vm-image/nightly-plan` decides which channels a run builds and where they are published. `test/vm-image/nightly-layout` lays out the files and writes `latest.json`. `test/unit/nightly-test.sh` tests both.
|
||||
|
||||
## latest.json
|
||||
|
||||
| Field | What it is |
|
||||
| --- | --- |
|
||||
| `version` | Edge: the build's date and run number, `2026.10.09.431`. Stable: the release, `4.0.4`. |
|
||||
| `channel` | `edge` or `stable`. |
|
||||
| `built_at` | When this file was written, after the image passed its checks. UTC, `2026-10-09T18:02:35Z`. |
|
||||
| `url` | The ISO that was installed. Edge: on nightly.omarchy.org. Stable: the release ISO on iso.omarchy.org. |
|
||||
| `sha256`, `size` | The ISO's SHA-256 and its size in bytes. |
|
||||
| `iso_commit` | The omarchy-iso commit the nightly build ran from. |
|
||||
| `omarchy_version` | The version of the Omarchy package in the image. |
|
||||
| `omarchy_commit` | Edge: the omarchy commit that package was built from. Stable: empty. |
|
||||
| `kernel` | The image's kernel release, as reported by `uname -r`. |
|
||||
| `vm.url`, `vm.sha256`, `vm.size` | The VM image: its URL, SHA-256 and size in bytes. |
|
||||
| `vm.unpacked` | The URL of the image's `unpacked/` folder, ending in `/`. |
|
||||
| `vm.unpacked_files` | Every file in that folder. |
|
||||
| `vm.user`, `vm.password` | The account for the desktop and `sudo`. |
|
||||
| `vm.ssh` | SSH authentication: keys only. Password authentication is off. |
|
||||
|
||||
The version is also in the edge ISO's file name.
|
||||
|
||||
## VM images
|
||||
|
||||
VM images are for testing. Users download the release ISO from iso.omarchy.org.
|
||||
|
||||
A VM image is Omarchy already installed, as one `.qcow2` file. There is an image of the edge channel, rebuilt every night, and an image of the latest release. Every boot is a new machine, with a new machine ID and new SSH host keys.
|
||||
|
||||
The user is `omarchy` and the password is `omarchy`. SSH takes keys only.
|
||||
|
||||
### Boot the image by itself
|
||||
|
||||
The image boots through UEFI with no other file. Run these commands on a Linux machine with QEMU, read and write access to `/dev/kvm`, `curl` and `jq`:
|
||||
|
||||
```bash
|
||||
./bin/omarchy-iso-make --local-source ../omarchy-installer ../omarchy-pkgs
|
||||
latest=https://nightly.omarchy.org/edge/latest.json # or .../stable/latest.json
|
||||
image=$(curl -fsSL "$latest" | jq -r .vm.url)
|
||||
curl -fLO "$image"
|
||||
curl -fsSLO "$image.sha256"
|
||||
sha256sum -c "${image##*/}.sha256"
|
||||
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd vars.fd
|
||||
qemu-system-x86_64 -enable-kvm -cpu host -smp 4 -m 4G \
|
||||
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
||||
-drive if=pflash,format=raw,file=vars.fd \
|
||||
-drive file="${image##*/}",if=virtio,snapshot=on \
|
||||
-device virtio-vga
|
||||
```
|
||||
|
||||
Despite the local folder name, the first argument is the Omarchy source checkout (runtime commands, configs, setup scripts, themes, shell, migrations). The installer itself lives in this ISO repo.
|
||||
The firmware paths are Arch's. `snapshot=on` keeps the downloaded image unchanged. The image is a 4 to 5 GB download.
|
||||
|
||||
Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror.
|
||||
### Boot it faster, with SSH
|
||||
|
||||
The `unpacked/` folder next to each image holds what a direct boot needs:
|
||||
|
||||
| File | What it is |
|
||||
| --- | --- |
|
||||
| `vmlinuz`, `initrd`, `cmdline` | The image's own kernel, initramfs and kernel arguments. |
|
||||
| `vm-boot` | A script that boots the image directly into its kernel and gives it an SSH key. |
|
||||
| `SHA256SUMS` | Checksums of the files above. |
|
||||
|
||||
A direct boot skips the firmware and the boot loader: SSH answers after about 6 seconds instead of about 20. Download the folder next to the image, then run `vm-boot`. It also needs `qemu-img` and `ssh`:
|
||||
|
||||
```bash
|
||||
unpacked=$(curl -fsSL "$latest" | jq -r .vm.unpacked)
|
||||
for file in $(curl -fsSL "$latest" | jq -r '.vm.unpacked_files[]'); do curl -fsSLO "$unpacked$file"; done
|
||||
sha256sum -c SHA256SUMS
|
||||
chmod +x vm-boot
|
||||
|
||||
./vm-boot "${image##*/}" # boots, prints how to log in, runs until Ctrl-C
|
||||
./vm-boot "${image##*/}" uname -r # boots, runs the command over SSH, powers off
|
||||
```
|
||||
|
||||
`vm-boot` makes a new SSH key for each boot. It runs the machine on an overlay under `/var/tmp`, so the downloaded image is never changed, and deletes the overlay on exit. Set `VM_STATE_DIR` to put the overlay somewhere else.
|
||||
|
||||
To get the URL of the edge ISO, run `jq -r .url` on the same `latest.json`.
|
||||
|
||||
## Autoinstall
|
||||
|
||||
The [manual's page](https://omarchy.org/manual/unattended-installs/) is the one for users; this is the same feature in full.
|
||||
|
||||
The shipped ISO installs itself with no keyboard when it finds its configuration on a second drive. Attach a drive labeled `cidata` alongside the ISO and the installer copies the config off it and skips the configurator; with no such drive, nothing changes and the wizard runs as usual. No rebuild, no extra boot entry.
|
||||
|
||||
`cidata` is the cloud-init `NoCloud` label, so Proxmox, libvirt, and Packer already know how to attach one.
|
||||
@@ -48,7 +165,7 @@ These are the configurator's own output files, so the way to get a starting set
|
||||
| `authorized_keys` | No | SSH public keys in sshd's own format, one per line |
|
||||
| `tailscale_authkey` | No | Tailscale auth key; the machine joins your tailnet on first boot |
|
||||
|
||||
Both required files must be present or the installer falls back to the configurator. Generate the password hash for `user_credentials.json` with `openssl passwd -6 "yourpassword"`.
|
||||
Both required files must be present or the installer falls back to the configurator. (A `defer-provisioning` marker file can stand in for `user_credentials.json`: user creation is then deferred to first boot.) Generate the password hash for `user_credentials.json` with `openssl passwd -6 "yourpassword"`.
|
||||
|
||||
Encryption itself is configured by the `disk_encryption` block inside `user_configuration.json` — which carries the passphrase in plaintext, so treat a drive built from an encrypted install accordingly. The flag file must match it: it drives the encrypted install's SDDM autologin and the final boot validation, not the encryption.
|
||||
|
||||
@@ -58,7 +175,7 @@ Encryption itself is configured by the `disk_encryption` block inside `user_conf
|
||||
ssh-ed25519 AAAA... you@host
|
||||
```
|
||||
|
||||
When `authorized_keys` is present, autoinstall installs it as the user's `~/.ssh/authorized_keys`, enables `sshd`, and adds a `ufw allow ssh` rule — a stock Omarchy install ships openssh with the service disabled and its firewall opens neither port 22 nor anything else beyond LocalSend. Networking needs nothing extra; NetworkManager is already enabled with DHCP. Password SSH authentication is left at the distro default. An `authorized_keys` with no usable keys fails the install rather than producing a machine nobody can reach.
|
||||
When `authorized_keys` is present, autoinstall installs it as the user's `~/.ssh/authorized_keys`, enables `sshd`, and adds a `ufw allow ssh` rule — a stock Omarchy install ships openssh with the service disabled and its firewall opens neither port 22 nor anything else beyond LocalSend and Docker's DNS. Networking needs nothing extra; NetworkManager is already enabled with DHCP. Password SSH authentication is left at the distro default. An `authorized_keys` with no keys in it fails the install rather than producing a machine nobody can reach.
|
||||
|
||||
When `tailscale_authkey` is present (one key, blank lines and `#` comments ignored), the install adds the `tailscale` package from the ISO's bundled mirror — nothing is fetched from the network at install or boot — and stages the join for first boot: the key lands at `/etc/tailscale/authkey` (root-only), `tailscaled` is enabled, ufw allows traffic in on `tailscale0`, and a background unit runs `tailscale up` once the network is actually up, retrying until it succeeds without holding up the boot. After a successful join the key is deleted and the unit disables itself; until then both survive reboots, so a machine installed offline joins whenever it first gets connectivity. The node appears on the tailnet under the configured hostname. Use a reusable, pre-authorized (tagged) key so one drive image serves many machines — or an ephemeral key for disposable VMs.
|
||||
|
||||
@@ -90,6 +207,20 @@ Boot order is disk first: the empty disk falls through to the ISO on the first b
|
||||
|
||||
Encrypted autoinstalls are not fully unattended — the LUKS passphrase prompt still needs someone at the first boot.
|
||||
|
||||
## Creating the ISO
|
||||
|
||||
Run `./bin/omarchy-iso-make`; output goes into `./release`. By default the ISO uses the Omarchy packages and tracks the `quattro` branch, from the stable mirror. Pass `--edge` to use `omarchy-dev` and `omarchy-settings-dev` from the edge mirror.
|
||||
|
||||
For local development, build the ISO from sibling checkouts:
|
||||
|
||||
```bash
|
||||
./bin/omarchy-iso-make --local-source ../omarchy-installer ../omarchy-pkgs
|
||||
```
|
||||
|
||||
Despite the local folder name, the first argument is the Omarchy source checkout (runtime commands, configs, setup scripts, themes, shell, migrations). The installer itself lives in this ISO repo.
|
||||
|
||||
Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror.
|
||||
|
||||
## Testing the ISO
|
||||
|
||||
Run `./bin/omarchy-iso-boot [release/omarchy.iso]`.
|
||||
|
||||
+16
-2
@@ -23,6 +23,19 @@ esac
|
||||
: "${OMARCHY_NVIM_PACKAGE:=omarchy-nvim}"
|
||||
export OMARCHY_RUNTIME_PACKAGE OMARCHY_SETTINGS_PACKAGE OMARCHY_NVIM_PACKAGE
|
||||
|
||||
# pacman 7 sandboxes its downloads with Landlock and fails every sync on a
|
||||
# kernel without it (Namespace's AmpereOne arm64 machines): "restricting
|
||||
# filesystem access failed because Landlock is not supported by the kernel".
|
||||
# There, every pacman in this container (pacstrap's too) runs with
|
||||
# --disable-sandbox, which pacman provides for exactly this. A wrapper rather
|
||||
# than DisableSandbox in a config: pacman-offline.conf ships on the ISO.
|
||||
if ! grep -qw landlock /sys/kernel/security/lsm 2>/dev/null; then
|
||||
echo "No Landlock in this kernel: pacman runs with --disable-sandbox in this container"
|
||||
printf '#!/bin/sh\nexec /usr/bin/pacman --disable-sandbox "$@"\n' >/usr/local/bin/pacman
|
||||
chmod +x /usr/local/bin/pacman
|
||||
hash -r
|
||||
fi
|
||||
|
||||
# Packages installed into the Arch container used to build the ISO.
|
||||
pacman-key --init
|
||||
pacman --noconfirm -Sy archlinux-keyring
|
||||
@@ -291,8 +304,9 @@ printf '%s\n' "${required_package_files[@]}" |
|
||||
|
||||
# Rebuild the offline repo db from scratch so size/checksum/depends entries
|
||||
# always reflect only the package files selected for this build.
|
||||
rm -f "$offline_mirror_dir"/offline.db* "$offline_mirror_dir"/offline.files*
|
||||
repo-add "$offline_mirror_dir/offline.db.tar.gz" "$offline_mirror_dir/"*.pkg.tar.zst
|
||||
# Every package archive in the mirror, indexed by one repo-add per CPU: one
|
||||
# alone hashes and lists a thousand packages one after another.
|
||||
bash /builder/index-offline-mirror.sh "$offline_mirror_dir"
|
||||
|
||||
# mkarchiso expects the mirror at /var/cache/omarchy/mirror/offline inside the
|
||||
# container (the airootfs path); symlink rather than duplicate.
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Index the offline mirror: <dir>/offline.db.tar.gz and offline.files.tar.gz
|
||||
# from every package in <dir>, the same databases one repo-add call writes.
|
||||
#
|
||||
# index-offline-mirror.sh <dir> [jobs]
|
||||
#
|
||||
# repo-add handles one package at a time, and for each it hashes the archive
|
||||
# and lists its files, which means decompressing all of it. A database is only
|
||||
# a tar of one directory per package, so the packages are dealt out to one
|
||||
# repo-add per CPU, each writing its own database, and the pieces are put
|
||||
# together afterwards.
|
||||
set -euo pipefail
|
||||
|
||||
dir=$(realpath "$1")
|
||||
jobs=${2:-$(nproc)}
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
mapfile -t packages < <(find "$dir" -maxdepth 1 -name '*.pkg.tar.*' ! -name '*.sig' | sort)
|
||||
(( ${#packages[@]} > 0 )) || { echo "index-offline-mirror: no packages in $dir" >&2; exit 1; }
|
||||
(( jobs > ${#packages[@]} )) && jobs=${#packages[@]}
|
||||
(( jobs >= 1 )) || jobs=1
|
||||
|
||||
# Deal the sorted list out round-robin: big and small packages spread evenly.
|
||||
for (( i = 0; i < jobs; i++ )); do mkdir "$work/$i"; done
|
||||
for (( i = 0; i < ${#packages[@]}; i++ )); do
|
||||
printf '%s\0' "${packages[i]}" >>"$work/$(( i % jobs ))/list"
|
||||
done
|
||||
|
||||
pids=()
|
||||
for (( i = 0; i < jobs; i++ )); do
|
||||
xargs -0 -a "$work/$i/list" repo-add -q "$work/$i/offline.db.tar.gz" >"$work/$i/log" 2>&1 &
|
||||
pids+=($!)
|
||||
done
|
||||
failed=0
|
||||
for (( i = 0; i < jobs; i++ )); do
|
||||
if ! wait "${pids[i]}"; then
|
||||
failed=1
|
||||
echo "index-offline-mirror: repo-add failed on part $i:" >&2
|
||||
cat "$work/$i/log" >&2
|
||||
fi
|
||||
done
|
||||
(( failed == 0 )) || exit 1
|
||||
|
||||
mkdir "$work/db" "$work/files"
|
||||
for (( i = 0; i < jobs; i++ )); do
|
||||
bsdtar -xf "$work/$i/offline.db.tar.gz" -C "$work/db"
|
||||
bsdtar -xf "$work/$i/offline.files.tar.gz" -C "$work/files"
|
||||
done
|
||||
|
||||
# One repo-add keeps a single entry per package name. Apart, two versions of
|
||||
# one name would both survive, so that is an error here rather than a choice.
|
||||
entries=$(find "$work/db" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
||||
names=$(cat "$work"/db/*/desc | awk '/^%NAME%$/ { getline; print }' | sort -u | wc -l)
|
||||
if (( entries != ${#packages[@]} || names != entries )); then
|
||||
echo "index-offline-mirror: ${#packages[@]} packages gave $entries entries for $names names; two versions of one package?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$dir"/offline.db* "$dir"/offline.files*
|
||||
for kind in db files; do
|
||||
find "$work/$kind" -mindepth 1 -maxdepth 1 -type d -printf '%P\n' | sort |
|
||||
bsdtar -czf "$dir/offline.$kind.tar.gz" -C "$work/$kind" -T -
|
||||
ln -s "offline.$kind.tar.gz" "$dir/offline.$kind"
|
||||
done
|
||||
echo "Indexed ${#packages[@]} packages with $jobs repo-add processes."
|
||||
@@ -29,6 +29,6 @@ Server = https://mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
SigLevel = Never
|
||||
@@ -29,6 +29,6 @@ Server = https://rc-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
SigLevel = Never
|
||||
@@ -29,6 +29,6 @@ Server = https://stable-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
SigLevel = Never
|
||||
@@ -503,16 +503,23 @@ EOF
|
||||
mcopy -i "$CIDATA_IMG" "$dir"/* ::/
|
||||
}
|
||||
|
||||
# The dev/local ISO installs the -dev packages; a stable ISO the plain ones.
|
||||
# The edge/dev/local ISO installs the -dev packages, as builder/build-iso.sh
|
||||
# picks them; a stable ISO the plain ones.
|
||||
detect_packages() {
|
||||
RUNTIME_PACKAGE=omarchy-dev
|
||||
SETTINGS_PACKAGE=omarchy-settings-dev
|
||||
if [[ $(basename "$ISO") != *dev* && $(basename "$ISO") != *local* && $(basename "$ISO") != *pr* ]]; then
|
||||
if [[ $(basename "$ISO") != *edge* && $(basename "$ISO") != *dev* && $(basename "$ISO") != *local* && $(basename "$ISO") != *pr* ]]; then
|
||||
RUNTIME_PACKAGE=omarchy
|
||||
SETTINGS_PACKAGE=omarchy-settings
|
||||
fi
|
||||
}
|
||||
|
||||
# The firmware names what it starts on the serial port: the ISO's medium while
|
||||
# the installer runs, the installed disk's Limine once the install is over.
|
||||
installed_system_started() {
|
||||
grep -aq 'starting Boot.*"Limine"' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
install_phase() {
|
||||
log "Installing $(basename "$ISO") unattended via cidata (headless)"
|
||||
|
||||
@@ -534,7 +541,7 @@ install_phase() {
|
||||
-device usb-storage,drive=cidata
|
||||
|
||||
log "Waiting for the unattended install to finish (timeout ${INSTALL_TIMEOUT}s)"
|
||||
local waited=0 text progress_name
|
||||
local waited=0 text progress_name first_boot_at=""
|
||||
while true; do
|
||||
# An unattended install reboots on its own; SSH answering means the
|
||||
# installed system is up (cidata's authorized_keys enables sshd).
|
||||
@@ -575,7 +582,20 @@ install_phase() {
|
||||
printf -v progress_name 'success-install-progress-%04ds' "$waited"
|
||||
capture_console "$progress_name"
|
||||
fi
|
||||
((waited % 30 == 0)) && echo " ... installing (${waited}s)"
|
||||
# Once the installed system has started, the install is over and what is
|
||||
# being waited for is the first boot: say so, or a hung first boot reads
|
||||
# as a slow install.
|
||||
if [[ -z $first_boot_at ]] && installed_system_started "$RUN_DIR/install-serial.log"; then
|
||||
first_boot_at=$waited
|
||||
log "Install finished after about ${waited}s. Waiting for the installed system's first boot."
|
||||
fi
|
||||
if ((waited % 30 == 0)); then
|
||||
if [[ -n $first_boot_at ]]; then
|
||||
echo " ... first boot ($((waited - first_boot_at))s, install took about ${first_boot_at}s)"
|
||||
else
|
||||
echo " ... installing (${waited}s)"
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
((waited += 10))
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The nightly's decisions, tried without a runner: which channels a run makes
|
||||
# and where they go (test/vm-image/nightly-plan), what it lays out and what
|
||||
# latest.json then says (test/vm-image/nightly-layout), and two things the
|
||||
# install test decides from what it is given: which Omarchy packages an ISO
|
||||
# installs, and when an install is over.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
plan="$ROOT/test/vm-image/nightly-plan"
|
||||
layout="$ROOT/test/vm-image/nightly-layout"
|
||||
harness="$ROOT/test/integration.d/base-test.sh"
|
||||
bash -n "$plan"
|
||||
bash -n "$layout"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
equal() { [[ $2 == "$3" ]] || fail "$1: got '$2', expected '$3'"; }
|
||||
refused() { # description, command...: it has to fail and say why
|
||||
local what=$1; shift
|
||||
if "$@" >"$tmp/refused.out" 2>"$tmp/refused.err"; then fail "$what was accepted"; fi
|
||||
[[ -s $tmp/refused.err ]] || fail "$what was refused without a word"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- nightly-plan
|
||||
planned() { "$plan" "$@" 2>/dev/null; }
|
||||
|
||||
first=$(planned 2026.10.09 431 1 schedule v4.0.4 "")
|
||||
equal "a first run makes both channels" "$(jq -r 'map(.channel) | join(",")' <<<"$first")" "edge,stable"
|
||||
equal "the edge version" "$(jq -r '.[0].version' <<<"$first")" "2026.10.09.431"
|
||||
equal "the edge path" "$(jq -r '.[0].release' <<<"$first")" "edge/2026.10.09.431"
|
||||
equal "the stable version is the release's" "$(jq -r '.[1].version' <<<"$first")" "4.0.4"
|
||||
equal "the stable path has the run number" "$(jq -r '.[1].release' <<<"$first")" "stable/4.0.4/431"
|
||||
|
||||
again=$(planned 2026.10.09 431 2 schedule v4.0.4 "")
|
||||
equal "a run started again has its own edge version" "$(jq -r '.[0].version' <<<"$again")" "2026.10.09.431.2"
|
||||
equal "a run started again has its own stable path" "$(jq -r '.[1].release' <<<"$again")" "stable/4.0.4/431.2"
|
||||
[[ $(jq -r '.[].release' <<<"$first" | sort) != "$(jq -r '.[].release' <<<"$again" | sort)" ]] ||
|
||||
fail "two attempts of one run share a path"
|
||||
|
||||
equal "a published release is left out of a scheduled run" \
|
||||
"$(planned 2026.10.09 432 1 schedule v4.0.4 4.0.4 | jq -r 'map(.channel) | join(",")')" "edge"
|
||||
equal "a new release is made on the next scheduled run" \
|
||||
"$(planned 2026.10.09 432 1 schedule v4.0.5 4.0.4 | jq -r '.[1].version')" "4.0.5"
|
||||
equal "a manual run makes a published release again" \
|
||||
"$(planned 2026.10.09 432 1 workflow_dispatch v4.0.4 4.0.4 | jq -r 'map(.channel) | join(",")')" "edge,stable"
|
||||
|
||||
refused "a release tag that is not a version" "$plan" 2026.10.09 431 1 schedule nightly ""
|
||||
refused "an empty release tag" "$plan" 2026.10.09 431 1 schedule "" ""
|
||||
refused "a release tag with a path in it" "$plan" 2026.10.09 431 1 schedule "v4.0.4/../x" ""
|
||||
refused "a run number that is not a number" "$plan" 2026.10.09 "" 1 schedule v4.0.4 ""
|
||||
refused "a date in another form" "$plan" 2026-10-09 431 1 schedule v4.0.4 ""
|
||||
|
||||
# -------------------------------------------------------------- nightly-layout
|
||||
sealed() { # directory: what vm-seal leaves
|
||||
rm -rf "$1"; mkdir -p "$1"
|
||||
local file
|
||||
for file in base.qcow2 vmlinuz initrd cmdline; do echo "$file of ${1##*/}" >"$1/$file"; done
|
||||
echo "sums of the seal" >"$1/SHA256SUMS"
|
||||
}
|
||||
export SITE=https://nightly.example ISO_SITE=https://iso.example ISO_COMMIT=0123456789abcdef0123456789abcdef01234567
|
||||
|
||||
# Edge. pacman answers for omarchy and omarchy-dev with the one installed
|
||||
# package, so its line is there twice.
|
||||
mkdir -p "$tmp/release"
|
||||
echo "an edge iso" >"$tmp/release/omarchy-2026.10.09-x86_64-edge.iso"
|
||||
sealed "$tmp/sealed-edge"
|
||||
printf '%s\n' "omarchy-dev 4.0.0.r6818.gc352b62-2" "omarchy-dev 4.0.0.r6818.gc352b62-2" \
|
||||
"/usr/lib/modules/7.2.8-5-omarchy-bore/pkgbase is owned by linux-omarchy-bore 7.2.8-5" "7.2.8-5-omarchy-bore" >"$tmp/edge-packages"
|
||||
"$layout" edge 2026.10.09.431 edge/2026.10.09.431 "$tmp/release/omarchy-2026.10.09-x86_64-edge.iso" \
|
||||
"$tmp/sealed-edge" "$tmp/edge-packages" "$tmp/out" >/dev/null
|
||||
json=$tmp/out/edge/latest.json
|
||||
dir=$tmp/out/edge/2026.10.09.431
|
||||
field() { jq -r "$1" "$json"; }
|
||||
|
||||
equal "edge: files published" "$(cd "$tmp/out" && find . -type f | sort | paste -sd' ')" \
|
||||
"./edge/2026.10.09.431/omarchy-edge-2026.10.09.431.iso ./edge/2026.10.09.431/omarchy-edge-2026.10.09.431.iso.sha256 ./edge/2026.10.09.431/omarchy-edge-2026.10.09.431.qcow2 ./edge/2026.10.09.431/omarchy-edge-2026.10.09.431.qcow2.sha256 ./edge/2026.10.09.431/unpacked/SHA256SUMS ./edge/2026.10.09.431/unpacked/cmdline ./edge/2026.10.09.431/unpacked/initrd ./edge/2026.10.09.431/unpacked/vm-boot ./edge/2026.10.09.431/unpacked/vmlinuz ./edge/latest.json"
|
||||
(cd "$dir" && sha256sum -c --quiet ./*.sha256) || fail "edge: a checksum file does not match the file it is for"
|
||||
equal "edge: the ISO and the image each have a checksum file" "$(cd "$dir" && ls ./*.sha256 | wc -l)" "2"
|
||||
equal "edge: the image is the sealed disk" "$(cat "$dir/omarchy-edge-2026.10.09.431.qcow2")" "base.qcow2 of sealed-edge"
|
||||
(cd "$dir/unpacked" && sha256sum -c --quiet SHA256SUMS) || fail "edge: SHA256SUMS does not match the unpacked files"
|
||||
equal "edge: SHA256SUMS covers every file beside it" \
|
||||
"$(awk '{ print $2 }' "$dir/unpacked/SHA256SUMS" | sort | paste -sd' ')" "$(cd "$dir/unpacked" && ls | grep -v SHA256SUMS | sort | paste -sd' ')"
|
||||
cmp -s "$dir/unpacked/vm-boot" "$ROOT/test/vm-image/vm-boot" || fail "edge: the published vm-boot is not the repository's"
|
||||
equal "edge: version" "$(field .version)" "2026.10.09.431"
|
||||
equal "edge: channel" "$(field .channel)" "edge"
|
||||
equal "edge: url" "$(field .url)" "https://nightly.example/edge/2026.10.09.431/omarchy-edge-2026.10.09.431.iso"
|
||||
equal "edge: sha256 is the ISO's" "$(field .sha256)" "$(sha256sum "$dir/omarchy-edge-2026.10.09.431.iso" | cut -d' ' -f1)"
|
||||
equal "edge: size is the ISO's" "$(field .size)" "$(stat -c %s "$dir/omarchy-edge-2026.10.09.431.iso")"
|
||||
equal "edge: iso_commit" "$(field .iso_commit)" "$ISO_COMMIT"
|
||||
equal "edge: omarchy_version, once" "$(field .omarchy_version)" "4.0.0.r6818.gc352b62-2"
|
||||
equal "edge: omarchy_commit" "$(field .omarchy_commit)" "c352b62"
|
||||
equal "edge: kernel" "$(field .kernel)" "7.2.8-5-omarchy-bore"
|
||||
equal "edge: image address" "$(field .vm.url)" "https://nightly.example/edge/2026.10.09.431/omarchy-edge-2026.10.09.431.qcow2"
|
||||
equal "edge: image checksum is the image's" "$(field .vm.sha256)" "$(sha256sum "$dir/omarchy-edge-2026.10.09.431.qcow2" | cut -d' ' -f1)"
|
||||
equal "edge: image size is the image's" "$(field .vm.size)" "$(stat -c %s "$dir/omarchy-edge-2026.10.09.431.qcow2")"
|
||||
equal "edge: the ISO and the image differ only in their ending" "$(field .url | sed 's/\.iso$//')" "$(field .vm.url | sed 's/\.qcow2$//')"
|
||||
equal "edge: unpacked folder" "$(field .vm.unpacked)" "https://nightly.example/edge/2026.10.09.431/unpacked/"
|
||||
equal "edge: every unpacked file listed is published" "$(field '.vm.unpacked_files[]' | sort | paste -sd' ')" "$(cd "$dir/unpacked" && ls | sort | paste -sd' ')"
|
||||
[[ $(field .built_at) =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:]{8}Z$ ]] || fail "edge: built_at is '$(field .built_at)'"
|
||||
# What a consumer does with the ISO's name: the first run of three or more
|
||||
# dotted numbers is the version.
|
||||
[[ $(basename "$(field .url)") =~ [0-9]+(\.[0-9]+){2,} ]] || fail "edge: the ISO's name carries no version"
|
||||
equal "edge: the version read from the ISO's name" "${BASH_REMATCH[0]}" "2026.10.09.431"
|
||||
|
||||
# Stable: the image is published, the released ISO stays where it is.
|
||||
rm -rf "$tmp/out"
|
||||
echo "a released iso" >"$tmp/release/omarchy-4.0.4.iso"
|
||||
sealed "$tmp/sealed-stable"
|
||||
printf '%s\n' "omarchy 4.0.4-1" "/usr/lib/modules/7.2.5-3-omarchy/pkgbase is owned by linux-omarchy 7.2.5-3" "7.2.5-3-omarchy" >"$tmp/stable-packages"
|
||||
"$layout" stable 4.0.4 stable/4.0.4/431 "$tmp/release/omarchy-4.0.4.iso" "$tmp/sealed-stable" "$tmp/stable-packages" "$tmp/out" >/dev/null
|
||||
json=$tmp/out/stable/latest.json
|
||||
equal "stable: no ISO is published" "$(find "$tmp/out" -name '*.iso*' | wc -l)" "0"
|
||||
[[ -f $tmp/release/omarchy-4.0.4.iso ]] || fail "stable: the released ISO was moved away"
|
||||
equal "stable: url is the release's" "$(field .url)" "https://iso.example/omarchy-4.0.4.iso"
|
||||
equal "stable: sha256 is the ISO's" "$(field .sha256)" "$(sha256sum "$tmp/release/omarchy-4.0.4.iso" | cut -d' ' -f1)"
|
||||
equal "stable: version" "$(field .version)" "4.0.4"
|
||||
equal "stable: omarchy_version" "$(field .omarchy_version)" "4.0.4-1"
|
||||
equal "stable: a release names no commit" "$(field .omarchy_commit)" ""
|
||||
equal "stable: image address has the run number" "$(field .vm.url)" "https://nightly.example/stable/4.0.4/431/omarchy-4.0.4.qcow2"
|
||||
equal "stable: unpacked folder" "$(field .vm.unpacked)" "https://nightly.example/stable/4.0.4/431/unpacked/"
|
||||
(cd "$tmp/out/stable/4.0.4/431" && sha256sum -c --quiet omarchy-4.0.4.qcow2.sha256) || fail "stable: the image's checksum file does not match it"
|
||||
(cd "$tmp/out/stable/4.0.4/431/unpacked" && sha256sum -c --quiet SHA256SUMS) || fail "stable: SHA256SUMS does not match the unpacked files"
|
||||
|
||||
# Nothing half-made is described.
|
||||
sealed "$tmp/sealed-short"; rm "$tmp/sealed-short/initrd"
|
||||
refused "a sealed image without its initrd" "$layout" stable 4.0.4 stable/4.0.4/9 "$tmp/release/omarchy-4.0.4.iso" "$tmp/sealed-short" "$tmp/stable-packages" "$tmp/out2"
|
||||
sealed "$tmp/sealed-x"; : >"$tmp/no-packages"
|
||||
refused "an image that named no Omarchy package" "$layout" stable 4.0.4 stable/4.0.4/9 "$tmp/release/omarchy-4.0.4.iso" "$tmp/sealed-x" "$tmp/no-packages" "$tmp/out3"
|
||||
sealed "$tmp/sealed-y"
|
||||
refused "a released ISO under another version's name" "$layout" stable 4.0.5 stable/4.0.5/9 "$tmp/release/omarchy-4.0.4.iso" "$tmp/sealed-y" "$tmp/stable-packages" "$tmp/out4"
|
||||
sealed "$tmp/sealed-z"
|
||||
refused "a channel that does not exist" "$layout" rc 4.0.4 rc/4.0.4 "$tmp/release/omarchy-4.0.4.iso" "$tmp/sealed-z" "$tmp/stable-packages" "$tmp/out5"
|
||||
|
||||
# ------------------------------------------------------------ the install test
|
||||
# The harness is a script that starts a VM when it is read; take the two
|
||||
# functions out of it.
|
||||
eval "$(sed -n '/^detect_packages() {$/,/^}$/p; /^installed_system_started() {$/,/^}$/p' "$harness")"
|
||||
declare -F detect_packages installed_system_started >/dev/null || fail "the harness no longer has detect_packages and installed_system_started"
|
||||
|
||||
packages_of() { ISO=$1; detect_packages; echo "$RUNTIME_PACKAGE $SETTINGS_PACKAGE"; }
|
||||
equal "an edge ISO installs the -dev packages" "$(packages_of release/omarchy-2026.10.09-x86_64-edge.iso)" "omarchy-dev omarchy-settings-dev"
|
||||
equal "a dev ISO installs the -dev packages" "$(packages_of release/omarchy-2026.10.09-x86_64-dev.iso)" "omarchy-dev omarchy-settings-dev"
|
||||
equal "a released ISO installs the release packages" "$(packages_of release/omarchy-4.0.4.iso)" "omarchy omarchy-settings"
|
||||
equal "a stable build installs the release packages" "$(packages_of /x/edge/omarchy-2026.10.09-x86_64-stable.iso)" "omarchy omarchy-settings"
|
||||
|
||||
printf '%s\r\n' 'BdsDxe: loading Boot0002 "UEFI QEMU DVD-ROM QM00011 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x5,0xFFFF,0x0)' \
|
||||
'BdsDxe: starting Boot0002 "UEFI QEMU DVD-ROM QM00011 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x5,0xFFFF,0x0)' >"$tmp/serial"
|
||||
if installed_system_started "$tmp/serial"; then fail "the installer's own boot counts as the installed system's"; fi
|
||||
printf '%s\r\n' 'BdsDxe: loading Boot0005 "Limine" from HD(1,GPT,774175D2,0x800,0x400000)/\EFI\limine\limine_x64.efi' >>"$tmp/serial"
|
||||
if installed_system_started "$tmp/serial"; then fail "loading Limine counts as having started it"; fi
|
||||
printf '%s\r\n' 'BdsDxe: starting Boot0005 "Limine" from HD(1,GPT,774175D2,0x800,0x400000)/\EFI\limine\limine_x64.efi' >>"$tmp/serial"
|
||||
installed_system_started "$tmp/serial" || fail "the installed system's Limine starting is not noticed"
|
||||
if installed_system_started "$tmp/no-such-log"; then fail "a missing serial log counts as a started system"; fi
|
||||
|
||||
echo "nightly tests passed"
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The offline mirror is indexed by several repo-add processes whose databases
|
||||
# are then joined (builder/index-offline-mirror.sh). The result has to be the
|
||||
# database one repo-add writes over the same packages, and two versions of one
|
||||
# package have to be an error, since apart they would both be kept.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
index="$ROOT/builder/index-offline-mirror.sh"
|
||||
bash -n "$index"
|
||||
if ! command -v repo-add >/dev/null; then
|
||||
echo "offline mirror index tests skipped: no repo-add here"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fixture=$(mktemp -d)
|
||||
trap 'rm -rf "$fixture"' EXIT
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
package() { # directory name version: the least repo-add accepts as a package
|
||||
local stage="$fixture/stage"
|
||||
rm -rf "$stage"; mkdir -p "$stage/usr/share/$2"
|
||||
echo "$2 $3" >"$stage/usr/share/$2/version"
|
||||
printf 'pkgname = %s\npkgver = %s\npkgdesc = fixture\narch = any\nsize = 1\n' "$2" "$3" >"$stage/.PKGINFO"
|
||||
bsdtar -czf "$1/$2-$3-any.pkg.tar.gz" -C "$stage" .PKGINFO usr
|
||||
}
|
||||
contents() { # database file -> directory holding its entries
|
||||
mkdir -p "$2"; bsdtar -xf "$1" -C "$2"
|
||||
}
|
||||
|
||||
mkdir "$fixture/one" "$fixture/many"
|
||||
for name in alpha bravo charlie delta echo foxtrot golf; do
|
||||
package "$fixture/one" "$name" 1.0-1
|
||||
cp "$fixture/one/$name-1.0-1-any.pkg.tar.gz" "$fixture/many/"
|
||||
done
|
||||
: >"$fixture/many/alpha-1.0-1-any.pkg.tar.gz.sig" # a signature is not a package
|
||||
|
||||
repo-add -q "$fixture/one/offline.db.tar.gz" "$fixture"/one/*.pkg.tar.gz >/dev/null 2>&1
|
||||
bash "$index" "$fixture/many" 3 >/dev/null || fail "indexing seven packages with three processes failed"
|
||||
for kind in db files; do
|
||||
[[ -L $fixture/many/offline.$kind ]] || fail "offline.$kind, the name pacman fetches, is missing"
|
||||
contents "$fixture/one/offline.$kind.tar.gz" "$fixture/x-one-$kind"
|
||||
contents "$fixture/many/offline.$kind" "$fixture/x-many-$kind"
|
||||
diff -r "$fixture/x-one-$kind" "$fixture/x-many-$kind" >/dev/null \
|
||||
|| fail "the joined $kind database differs from one repo-add's"
|
||||
done
|
||||
|
||||
# More processes than packages, and a second run over an indexed mirror.
|
||||
bash "$index" "$fixture/many" 64 >/dev/null || fail "more processes than packages failed"
|
||||
contents "$fixture/many/offline.db" "$fixture/x-again"
|
||||
diff -r "$fixture/x-one-db" "$fixture/x-again" >/dev/null || fail "indexing an already indexed mirror changed it"
|
||||
|
||||
package "$fixture/many" alpha 2.0-1
|
||||
bash "$index" "$fixture/many" 3 >/dev/null 2>&1 && fail "two versions of one package were indexed"
|
||||
|
||||
mkdir "$fixture/empty"
|
||||
bash "$index" "$fixture/empty" >/dev/null 2>&1 && fail "an empty mirror was indexed"
|
||||
|
||||
grep -Fq 'bash /builder/index-offline-mirror.sh "$offline_mirror_dir"' "$ROOT/builder/build-iso.sh" \
|
||||
|| fail "the build does not use the parallel index"
|
||||
|
||||
echo "offline mirror index tests passed"
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# Lays out what one channel of a nightly run publishes and writes the
|
||||
# latest.json that describes it.
|
||||
#
|
||||
# nightly-layout <channel> <version> <release> <iso> <sealed_dir> <vm_packages> <out_dir>
|
||||
#
|
||||
# release the path under the site, from nightly-plan
|
||||
# iso the ISO that was installed
|
||||
# sealed_dir what vm-seal made
|
||||
# vm_packages what the sealed image answered to: pacman -Q of its Omarchy
|
||||
# package, pacman -Qo of its kernel's pkgbase, uname -r
|
||||
#
|
||||
# Env: SITE (https://nightly.omarchy.org), ISO_SITE (https://iso.omarchy.org),
|
||||
# ISO_COMMIT (the omarchy-iso commit, default: HEAD of this checkout).
|
||||
#
|
||||
# out_dir gets, under <release>/:
|
||||
#
|
||||
# omarchy-<...>.qcow2 and .qcow2.sha256 the VM image: one file, which boots
|
||||
# by itself through UEFI
|
||||
# unpacked/ its kernel, initramfs and kernel
|
||||
# arguments for booting it directly,
|
||||
# vm-boot, and their SHA256SUMS
|
||||
# omarchy-edge-<version>.iso and .sha256 edge only
|
||||
#
|
||||
# and <channel>/latest.json. The released ISO already has its place:
|
||||
# latest.json points there and the ISO stays where it is.
|
||||
#
|
||||
# The ISO and the image are moved, not copied: they are gigabytes.
|
||||
set -euo pipefail
|
||||
[[ $# -eq 7 ]] || { sed -n '2,12p' "$0" >&2; exit 2; }
|
||||
channel=$1 version=$2 release=$3 iso_file=$4 sealed=$5 packages=$6 out=$7
|
||||
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
site=${SITE:-https://nightly.omarchy.org}
|
||||
iso_site=${ISO_SITE:-https://iso.omarchy.org}
|
||||
iso_commit=${ISO_COMMIT:-$(git -C "$here" rev-parse HEAD)}
|
||||
fail() { echo "nightly-layout: $*" >&2; exit 1; }
|
||||
|
||||
[[ $channel == edge || $channel == stable ]] || fail "'$channel' is not a channel"
|
||||
[[ -f $iso_file ]] || fail "no ISO at $iso_file"
|
||||
unpacked=(vmlinuz initrd cmdline)
|
||||
for file in base.qcow2 "${unpacked[@]}"; do
|
||||
[[ -s $sealed/$file ]] || fail "the sealed image has no $file"
|
||||
done
|
||||
|
||||
omarchy=$(awk '$1 == "omarchy" || $1 == "omarchy-dev" { print $2; exit }' "$packages")
|
||||
kernel=$(tail -n 1 "$packages")
|
||||
[[ -n $omarchy ]] || fail "$packages names no omarchy package"
|
||||
[[ $kernel =~ ^[0-9]+\.[0-9] ]] || fail "$packages does not end with a kernel release"
|
||||
# An edge package's version carries the commit it was built from.
|
||||
omarchy_commit=$(sed -nE 's/.*\.g([0-9a-f]+)-.*/\1/p' <<<"$omarchy")
|
||||
|
||||
dir=$out/$release
|
||||
mkdir -p "$dir/unpacked" "$out/$channel"
|
||||
if [[ $channel == edge ]]; then
|
||||
name=omarchy-$channel-$version
|
||||
iso=$name.iso
|
||||
iso_url=$site/$release/$iso
|
||||
mv "$iso_file" "$dir/$iso"
|
||||
(cd "$dir" && sha256sum "$iso" >"$iso.sha256")
|
||||
iso_sha256=$(cut -d' ' -f1 "$dir/$iso.sha256") iso_size=$(stat -c %s "$dir/$iso")
|
||||
else
|
||||
name=omarchy-$version
|
||||
iso=$name.iso
|
||||
[[ ${iso_file##*/} == "$iso" ]] || fail "the released ISO is $iso, not ${iso_file##*/}"
|
||||
iso_url=$iso_site/$iso
|
||||
iso_sha256=$(sha256sum "$iso_file" | cut -d' ' -f1) iso_size=$(stat -c %s "$iso_file")
|
||||
fi
|
||||
|
||||
# The image has the ISO's name, so the two are told apart by their endings.
|
||||
image=$name.qcow2
|
||||
mv "$sealed/base.qcow2" "$dir/$image"
|
||||
(cd "$dir" && sha256sum "$image" >"$image.sha256")
|
||||
|
||||
for file in "${unpacked[@]}"; do mv "$sealed/$file" "$dir/unpacked/$file"; done
|
||||
cp "$here/vm-boot" "$dir/unpacked/vm-boot"
|
||||
unpacked+=(vm-boot)
|
||||
(cd "$dir/unpacked" && sha256sum "${unpacked[@]}" >SHA256SUMS)
|
||||
|
||||
latest=$out/$channel/latest.json
|
||||
jq -n \
|
||||
--arg version "$version" --arg channel "$channel" --arg release "$site/$release" --arg image "$image" \
|
||||
--arg built_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
--arg url "$iso_url" --arg sha256 "$iso_sha256" --argjson size "$iso_size" \
|
||||
--arg iso_commit "$iso_commit" --arg omarchy_version "$omarchy" --arg omarchy_commit "$omarchy_commit" \
|
||||
--arg kernel "$kernel" \
|
||||
--arg vm_sha256 "$(cut -d' ' -f1 "$dir/$image.sha256")" --argjson vm_size "$(stat -c %s "$dir/$image")" \
|
||||
--args '{
|
||||
version: $version, channel: $channel, built_at: $built_at,
|
||||
url: $url, sha256: $sha256, size: $size,
|
||||
iso_commit: $iso_commit, omarchy_version: $omarchy_version, omarchy_commit: $omarchy_commit,
|
||||
kernel: $kernel,
|
||||
vm: {
|
||||
url: "\($release)/\($image)", sha256: $vm_sha256, size: $vm_size,
|
||||
unpacked: "\($release)/unpacked/", unpacked_files: ($ARGS.positional + ["SHA256SUMS"]),
|
||||
user: "omarchy", password: "omarchy",
|
||||
ssh: "keys only; vm-boot makes one for each boot"
|
||||
}
|
||||
}' "${unpacked[@]}" >"$latest"
|
||||
|
||||
# What is about to be published has to say what it should: one value in every
|
||||
# field, and checksums that are checksums.
|
||||
jq -e --arg channel "$channel" '
|
||||
def one_line: type == "string" and test("^[^\n]+$");
|
||||
def sha: type == "string" and test("^[0-9a-f]{64}$");
|
||||
(.version | one_line) and .channel == $channel
|
||||
and (.url | test("^https://[^\n ]+\\.iso$")) and (.sha256 | sha) and .size > 0
|
||||
and (.iso_commit | test("^[0-9a-f]{40}$"))
|
||||
and (.omarchy_version | one_line) and (.omarchy_commit | test("^[0-9a-f]*$")) and (.kernel | one_line)
|
||||
and (.vm.url | test("^https://[^\n ]+\\.qcow2$")) and (.vm.sha256 | sha) and .vm.size > 0
|
||||
and (.vm.unpacked | test("^https://[^\n ]+/unpacked/$")) and (.vm.unpacked_files | length) == 5
|
||||
' "$latest" >/dev/null || { cat "$latest" >&2; fail "latest.json does not say what it should"; }
|
||||
cat "$latest"
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Says which channels a nightly run makes, under which version, and where
|
||||
# each one is published. It decides; the workflow fetches what it needs to
|
||||
# know and passes it in, so every case can be tried without a runner.
|
||||
#
|
||||
# nightly-plan <date> <run_number> <run_attempt> <event> <release_tag> [published_stable]
|
||||
#
|
||||
# date today, as YYYY.MM.DD
|
||||
# event what started the run: schedule, workflow_dispatch
|
||||
# release_tag omacom/omarchy's latest release, such as v4.0.4
|
||||
# published_stable the version stable/latest.json names now, if any
|
||||
#
|
||||
# Prints a JSON list of {channel, version, release}; release is the path
|
||||
# under the site.
|
||||
#
|
||||
# An edge version is the date and the run number. A stable version is the
|
||||
# release's own, and on a scheduled run a release already published is left
|
||||
# out: it is done once. Two builds are never the same bytes and a published
|
||||
# file is never replaced, so no path is used twice: a stable image has the run
|
||||
# number in its path, and a run started again adds its attempt to the number.
|
||||
set -euo pipefail
|
||||
[[ $# -eq 5 || $# -eq 6 ]] || { sed -n '2,12p' "$0" >&2; exit 2; }
|
||||
date=$1 run=$2 attempt=$3 event=$4 tag=$5 published=${6:-}
|
||||
fail() { echo "nightly-plan: $*" >&2; exit 1; }
|
||||
|
||||
[[ $date =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}$ ]] || fail "'$date' is not a date like 2026.10.09"
|
||||
[[ $run =~ ^[1-9][0-9]*$ ]] || fail "'$run' is not a run number"
|
||||
[[ $attempt =~ ^[1-9][0-9]*$ ]] || fail "'$attempt' is not a run attempt"
|
||||
stable=${tag#v}
|
||||
[[ $stable =~ ^[0-9]+(\.[0-9]+)+$ ]] || fail "the latest release is '$tag', not a version"
|
||||
|
||||
build=$run
|
||||
[[ $attempt == 1 ]] || build+=.$attempt
|
||||
|
||||
channels=$(jq -cn --arg v "$date.$build" '[{channel: "edge", version: $v, release: "edge/\($v)"}]')
|
||||
if [[ $event == schedule && $published == "$stable" ]]; then
|
||||
echo "nightly-plan: stable $stable is already published" >&2
|
||||
else
|
||||
channels=$(jq -c --arg v "$stable" --arg build "$build" \
|
||||
'. + [{channel: "stable", version: $v, release: "stable/\($v)/\($build)"}]' <<<"$channels")
|
||||
fi
|
||||
echo "$channels"
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# Boots a sealed Omarchy image (vm-seal) as a throwaway machine.
|
||||
#
|
||||
# vm-boot <image> boot, print how to log in, run until Ctrl-C
|
||||
# vm-boot <image> <command...> boot, run the command over SSH, power off;
|
||||
# exits with the command's status
|
||||
#
|
||||
# <image> is a downloaded disk, omarchy-<...>.qcow2, with vmlinuz, initrd and
|
||||
# cmdline in the same directory as this script, as in a published "unpacked"
|
||||
# folder. Or it is a directory as vm-seal leaves it, holding base.qcow2 and
|
||||
# those three files.
|
||||
#
|
||||
# Nothing is written to the image: the machine runs on a copy-on-write overlay
|
||||
# that is deleted on exit. Each boot gets a new SSH key and a new machine ID;
|
||||
# the image itself holds no key and its SSH takes no password.
|
||||
#
|
||||
# Needs qemu-system-x86_64, qemu-img, ssh and a usable /dev/kvm.
|
||||
# Env: SMP (4), MEM (4G), SSH_PORT (random), VM_UUID (random), APPEND (more
|
||||
# kernel arguments), QEMU_EXTRA (more qemu arguments, split on spaces),
|
||||
# VM_STATE_DIR (/var/tmp; holds the overlay, so it needs room to grow).
|
||||
set -euo pipefail
|
||||
[[ $# -ge 1 ]] || { sed -n '2,21p' "$0" >&2; exit 2; }
|
||||
image=$(realpath "$1"); shift
|
||||
if [[ -d $image ]]; then
|
||||
base=$image disk=$image/base.qcow2
|
||||
else
|
||||
base=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) disk=$image
|
||||
fi
|
||||
[[ -s $disk ]] || { echo "vm-boot: there is no disk image at $disk" >&2; exit 1; }
|
||||
for f in vmlinuz initrd cmdline; do
|
||||
[[ -s $base/$f ]] || { echo "vm-boot: $base/$f is missing" >&2; exit 1; }
|
||||
done
|
||||
[[ -r /dev/kvm && -w /dev/kvm ]] || { echo "vm-boot: /dev/kvm is not usable" >&2; exit 1; }
|
||||
|
||||
state=$(mktemp -d "${VM_STATE_DIR:-/var/tmp}/vm.XXXXXX")
|
||||
cleanup() {
|
||||
local status=$? pid
|
||||
if [[ -f $state/qemu.pid ]]; then
|
||||
pid=$(cat "$state/qemu.pid")
|
||||
kill "$pid" 2>/dev/null || true
|
||||
for _ in $(seq 1 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
|
||||
fi
|
||||
rm -rf "$state"
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT TERM
|
||||
port=${SSH_PORT:-$(( 20000 + RANDOM % 10000 ))}
|
||||
uuid=${VM_UUID:-$(cat /proc/sys/kernel/random/uuid)}
|
||||
|
||||
# A key made for this boot only, handed in through the firmware tables as a
|
||||
# systemd credential that writes authorized_keys.
|
||||
ssh-keygen -q -t ed25519 -N '' -C vm-boot -f "$state/id_ed25519"
|
||||
keys=$(printf 'd /home/omarchy/.ssh 0700 omarchy omarchy -\nf+ /home/omarchy/.ssh/authorized_keys 0600 omarchy omarchy - %s\n' \
|
||||
"$(cat "$state/id_ed25519.pub")" | base64 -w0)
|
||||
ssh_opts=(-i "$state/id_ed25519" -p "$port" -o BatchMode=yes -o IdentitiesOnly=yes
|
||||
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR)
|
||||
|
||||
qemu-img create -qf qcow2 -b "$disk" -F qcow2 "$state/run.qcow2"
|
||||
t0=$(date +%s%N)
|
||||
# shellcheck disable=SC2086
|
||||
qemu-system-x86_64 -cpu host -enable-kvm -machine q35,accel=kvm -smp "${SMP:-4}" -m "${MEM:-4G}" \
|
||||
-kernel "$base/vmlinuz" -initrd "$base/initrd" -append "$(cat "$base/cmdline")${APPEND:+ $APPEND}" \
|
||||
-uuid "$uuid" \
|
||||
-drive "file=$state/run.qcow2,format=qcow2,if=none,id=d0" -device virtio-blk-pci,drive=d0 \
|
||||
-smbios "type=11,value=io.systemd.credential.binary:tmpfiles.extra=$keys" \
|
||||
-device virtio-vga -device virtio-rng-pci -display none \
|
||||
-netdev "user,id=n0,hostfwd=tcp:127.0.0.1:$port-:22" -device virtio-net-pci,netdev=n0 \
|
||||
-qmp "unix:$state/qmp.sock,server,nowait" -pidfile "$state/qemu.pid" -daemonize ${QEMU_EXTRA:-}
|
||||
|
||||
# </dev/null: the probe must not eat a script meant for the command below.
|
||||
until ssh "${ssh_opts[@]}" -o ConnectTimeout=1 omarchy@127.0.0.1 true </dev/null 2>/dev/null; do
|
||||
kill -0 "$(cat "$state/qemu.pid")" 2>/dev/null || { echo "vm-boot: qemu exited before ssh came up" >&2; exit 1; }
|
||||
(( ($(date +%s%N) - t0) / 1000000 < 300000 )) || { echo "vm-boot: no ssh after 300s" >&2; exit 1; }
|
||||
sleep 0.2
|
||||
done
|
||||
echo "vm-boot: ssh up after $(( ($(date +%s%N) - t0) / 1000000 )) ms, machine ID ${uuid//-/}" >&2
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
# Stdin passes through, so `vm-boot dir bash -s <script` works.
|
||||
ssh "${ssh_opts[@]}" omarchy@127.0.0.1 "$@"
|
||||
else
|
||||
echo "ssh ${ssh_opts[*]} omarchy@127.0.0.1"
|
||||
echo "Desktop and sudo password: omarchy. QMP socket: $state/qmp.sock. Ctrl-C powers it off."
|
||||
while kill -0 "$(cat "$state/qemu.pid")" 2>/dev/null; do sleep 1; done
|
||||
fi
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# Turns an installed Omarchy disk (test/integration's base image) into an image anyone can
|
||||
# download and boot: every boot is its own machine, nothing of the build is
|
||||
# left in it, and it boots straight into its kernel, so a boot takes seconds.
|
||||
#
|
||||
# vm-seal <installed_dir> <sealed_dir>
|
||||
#
|
||||
# installed_dir holds base.qcow2, OVMF_VARS.4m.fd and id_ed25519, which is
|
||||
# what test/integration leaves in test-runs/<iso>-integration once its install
|
||||
# has passed. sealed_dir gets a standalone, zstd-compressed base.qcow2, which
|
||||
# boots by itself through UEFI, and the UKI's vmlinuz, initrd and cmdline for
|
||||
# booting it directly. It holds no key: each boot is handed one (see vm-boot).
|
||||
#
|
||||
# The image keeps the installer's user and password (GUEST_PASSWORD, default
|
||||
# omarchy) for the desktop and sudo. SSH takes keys only.
|
||||
set -euo pipefail
|
||||
[[ $# -eq 2 ]] || { echo "Usage: vm-seal <installed_dir> <sealed_dir>" >&2; exit 2; }
|
||||
in=$(realpath "$1") out=$(realpath -m "$2")
|
||||
pw=${GUEST_PASSWORD:-omarchy}
|
||||
for fw in /usr/share/edk2/x64/OVMF_CODE.4m.fd /usr/share/OVMF/OVMF_CODE_4M.fd; do
|
||||
[[ -f $fw ]] && { fw_code=$fw; break; }
|
||||
done
|
||||
: "${fw_code:?no OVMF firmware found}"
|
||||
|
||||
state=$(mktemp -d /var/tmp/seal.XXXXXX)
|
||||
cleanup() {
|
||||
local status=$? pid
|
||||
if [[ -f $state/qemu.pid ]]; then
|
||||
pid=$(cat "$state/qemu.pid")
|
||||
kill "$pid" 2>/dev/null || true
|
||||
for _ in $(seq 1 50); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
|
||||
fi
|
||||
rm -rf "$state"
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
port=$(( 30000 + RANDOM % 5000 ))
|
||||
|
||||
ssh_vm() {
|
||||
ssh -i "$in/id_ed25519" -p "$port" -o BatchMode=yes -o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ConnectTimeout=2 -o LogLevel=ERROR omarchy@127.0.0.1 "$@"
|
||||
}
|
||||
# Runs a script as root in the guest and fails if any line of it fails. The
|
||||
# password goes to sudo on stdin, never into a command line.
|
||||
root() {
|
||||
printf '%s\n' "$pw" | ssh_vm "sudo -S -p '' bash -ec $(printf %q "trap 'echo \"vm-seal: failed in the guest at: \$BASH_COMMAND\" >&2' ERR
|
||||
$1")"
|
||||
}
|
||||
|
||||
qemu-img create -qf qcow2 -b "$in/base.qcow2" -F qcow2 "$state/edit.qcow2"
|
||||
cp "$in/OVMF_VARS.4m.fd" "$state/vars.fd"
|
||||
qemu-system-x86_64 -cpu host -enable-kvm -machine q35,accel=kvm -smp 4 -m 4G \
|
||||
-drive if=pflash,format=raw,readonly=on,file="$fw_code" \
|
||||
-drive if=pflash,format=raw,file="$state/vars.fd" \
|
||||
-drive "file=$state/edit.qcow2,format=qcow2,if=virtio,discard=unmap" -display none \
|
||||
-netdev "user,id=n0,hostfwd=tcp:127.0.0.1:$port-:22" -device virtio-net-pci,netdev=n0 \
|
||||
-pidfile "$state/qemu.pid" -daemonize
|
||||
for i in $(seq 1 150); do
|
||||
ssh_vm true 2>/dev/null && break
|
||||
(( i < 150 )) || { echo "vm-seal: no ssh on the installed disk" >&2; exit 1; }
|
||||
sleep 2
|
||||
done
|
||||
# Let first-boot work finish before taking it apart.
|
||||
ssh_vm 'timeout 180 systemctl is-system-running --wait >/dev/null 2>&1 || true'
|
||||
|
||||
mkdir -p "$out"
|
||||
# The kernel that is running: edge installs linux-omarchy-bore where stable
|
||||
# installs linux-omarchy, and the boot entry, not the UKI, may hold the
|
||||
# command line.
|
||||
kernel=$(ssh_vm 'cat "/usr/lib/modules/$(uname -r)/pkgbase"')
|
||||
root "cat /boot/EFI/Linux/omarchy_$kernel.efi" >"$state/uki.efi"
|
||||
ssh_vm 'cat /proc/cmdline' >"$state/cmdline"
|
||||
|
||||
root '
|
||||
# The ISO writes autologin only for encrypted installs; a test has nobody at
|
||||
# SDDM, and a desktop check that skips for want of a session reads as a pass.
|
||||
printf "[Autologin]\nUser=omarchy\nSession=omarchy.desktop\n" >/etc/sddm.conf.d/autologin.conf
|
||||
|
||||
# The image is public and its password is too, so SSH takes keys only. sshd
|
||||
# keeps the first value it reads, hence a name that sorts first.
|
||||
printf "PasswordAuthentication no\nKbdInteractiveAuthentication no\nAuthenticationMethods publickey\n" \
|
||||
>/etc/ssh/sshd_config.d/00-omarchy-vm.conf
|
||||
ssh-keygen -A >/dev/null # sshd -T needs host keys to parse; they go below
|
||||
sshd -T | grep -qix "passwordauthentication no"
|
||||
sshd -T | grep -qix "authenticationmethods publickey"
|
||||
|
||||
# Empty, not "uninitialized": that string marks a first boot, and a first boot
|
||||
# applies unit presets, which on Arch disable everything not listed. Empty lets
|
||||
# systemd take the VM SMBIOS UUID (qemu -uuid) as this boot machine ID.
|
||||
# Host keys come back from sshdgenkeys.service before sshd starts.
|
||||
: >/etc/machine-id
|
||||
rm -f /var/lib/dbus/machine-id /etc/ssh/ssh_host_*
|
||||
rm -f /home/omarchy/.ssh/authorized_keys /home/omarchy/.bash_history /root/.bash_history
|
||||
|
||||
# The factory snapshot the installer took carries the same machine ID.
|
||||
mkdir -p /run/seal-top
|
||||
mount -o subvolid=5 "$(findmnt -no SOURCE / | sed "s/\[.*//")" /run/seal-top
|
||||
if [ -f /run/seal-top/@factory/etc/machine-id ]; then
|
||||
ro=$(btrfs property get -ts /run/seal-top/@factory ro)
|
||||
[ "$ro" = ro=false ] || btrfs property set -ts /run/seal-top/@factory ro false
|
||||
: >/run/seal-top/@factory/etc/machine-id
|
||||
[ "$ro" = ro=false ] || btrfs property set -ts /run/seal-top/@factory ro true
|
||||
fi
|
||||
umount /run/seal-top
|
||||
|
||||
# Stopping the unit saves a seed; with it stopped, shutdown saves none.
|
||||
systemctl stop systemd-random-seed.service
|
||||
rm -f /var/lib/systemd/random-seed /var/lib/systemd/credential.secret
|
||||
|
||||
# Leases and the key NetworkManager derives stable addresses from.
|
||||
systemctl stop NetworkManager.service
|
||||
rm -f /var/lib/NetworkManager/*.lease /var/lib/NetworkManager/secret_key \
|
||||
/var/lib/NetworkManager/seen-bssids /var/lib/NetworkManager/timestamps \
|
||||
/var/lib/NetworkManager/NetworkManager.state
|
||||
|
||||
# Vacuuming leaves the active journal. Send journald to /run, then the
|
||||
# build logs on disk can go, and shutdown writes nothing back.
|
||||
journalctl --relinquish-var
|
||||
rm -rf /var/log/journal/*
|
||||
|
||||
test ! -s /etc/machine-id
|
||||
test -z "$(ls /etc/ssh/ssh_host_* /var/lib/NetworkManager/*.lease /var/log/journal/* 2>/dev/null)"
|
||||
test ! -e /home/omarchy/.ssh/authorized_keys
|
||||
test ! -e /var/lib/systemd/random-seed
|
||||
|
||||
sync
|
||||
# Hand freed blocks back to the disk image, so the copy skips them.
|
||||
fstrim -av >/dev/null
|
||||
(sleep 1; systemctl poweroff) >/dev/null 2>&1 &'
|
||||
|
||||
# The key that let us in is gone, so the poweroff above was the last word.
|
||||
pid=$(cat "$state/qemu.pid")
|
||||
for _ in $(seq 1 120); do kill -0 "$pid" 2>/dev/null || break; sleep 1; done
|
||||
if kill -0 "$pid" 2>/dev/null; then echo "vm-seal: the VM did not power off" >&2; exit 1; fi
|
||||
rm -f "$state/qemu.pid"
|
||||
|
||||
# Direct boot skips the firmware and Limine, which spend most of a boot reading
|
||||
# and hashing the UKI. resume= points at the image's own swap, so drop it.
|
||||
objcopy -O binary --only-section=.linux "$state/uki.efi" "$out/vmlinuz"
|
||||
objcopy -O binary --only-section=.initrd "$state/uki.efi" "$out/initrd"
|
||||
tr -d '\0' <"$state/cmdline" | tr ' ' '\n' | { grep -vE '^(resume|resume_offset)=|^$' || true; } | paste -sd' ' >"$out/cmdline"
|
||||
[[ -s $out/vmlinuz && -s $out/initrd && -s $out/cmdline ]] || { echo "vm-seal: the UKI gave no kernel, initrd or cmdline" >&2; exit 1; }
|
||||
|
||||
# zstd clusters: QEMU reads them as they are, so the download boots as it is.
|
||||
qemu-img convert -c -O qcow2 -o compression_type=zstd -m 16 -W "$state/edit.qcow2" "$out/base.qcow2"
|
||||
( cd "$out" && sha256sum base.qcow2 vmlinuz initrd cmdline >SHA256SUMS )
|
||||
echo "sealed: $out ($(du -sh "$out/base.qcow2" | cut -f1)); cmdline: $(cat "$out/cmdline")"
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Boots a sealed image once, as a throwaway machine, and checks that it is
|
||||
# what vm-seal promises: a desktop that comes up, nothing of the build left
|
||||
# in it, and SSH that takes keys only. Exits non-zero on any failed check.
|
||||
#
|
||||
# vm-verify <sealed_dir>
|
||||
set -euo pipefail
|
||||
[[ $# -eq 1 ]] || { echo "Usage: vm-verify <sealed_dir>" >&2; exit 2; }
|
||||
here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
base=$(realpath "$1")
|
||||
( cd "$base" && sha256sum --quiet -c SHA256SUMS )
|
||||
uuid=$(cat /proc/sys/kernel/random/uuid)
|
||||
export VM_UUID=$uuid SSH_PORT=$(( 20000 + RANDOM % 10000 ))
|
||||
|
||||
VM_PW=${GUEST_PASSWORD:-omarchy} "$here/vm-boot" "$base" "WANT_ID=${uuid//-/} PW=${GUEST_PASSWORD:-omarchy} bash -s" <<'GUEST'
|
||||
fail=0
|
||||
check() { # description, then a command that must succeed
|
||||
local what=$1; shift
|
||||
if "$@" >/dev/null 2>&1; then echo "ok $what"; else echo "FAIL $what"; fail=1; fi
|
||||
}
|
||||
root() { printf '%s\n' "$PW" | sudo -S -p '' "$@"; }
|
||||
booted=$(( $(date +%s) - $(cut -d. -f1 /proc/uptime) - 5 ))
|
||||
newer() { [ "$(root stat -c %Y "$1")" -ge "$booted" ]; } # written by this boot
|
||||
|
||||
check "the system reaches running or degraded" \
|
||||
sh -c 's=$(timeout 120 systemctl is-system-running --wait); [ "$s" = running ] || [ "$s" = degraded ]'
|
||||
check "no unit failed" sh -c '[ -z "$(systemctl --failed --no-legend)" ]'
|
||||
check "a desktop session is logged in" sh -c 'for i in $(seq 1 60); do pgrep -x Hyprland && exit 0; sleep 0.5; done; exit 1'
|
||||
check "the machine ID is the one this boot was given" [ "$(cat /etc/machine-id)" = "$WANT_ID" ]
|
||||
check "the journal holds this machine only" [ "$(root ls /var/log/journal | tr '\n' ' ')" = "$WANT_ID " ]
|
||||
check "SSH refuses passwords" sh -c "printf '%s\n' '$PW' | sudo -S -p '' sshd -T | grep -qix 'passwordauthentication no'"
|
||||
check "SSH requires a key" sh -c "printf '%s\n' '$PW' | sudo -S -p '' sshd -T | grep -qix 'authenticationmethods publickey'"
|
||||
# Asking with no method makes sshd list the ones it would take.
|
||||
check "sshd offers a key as the only way in" \
|
||||
sh -c 'ssh -o PreferredAuthentications=none -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null omarchy@127.0.0.1 true 2>&1 | grep -qF "Permission denied (publickey)."'
|
||||
check "authorized_keys holds one key, this boot's" [ "$(grep -c . ~/.ssh/authorized_keys)" = 1 ]
|
||||
check "SSH host keys were made by this boot" newer /etc/ssh/ssh_host_ed25519_key
|
||||
check "NetworkManager's key was made by this boot" newer /var/lib/NetworkManager/secret_key
|
||||
check "one DHCP lease, this boot's" [ "$(root sh -c 'ls /var/lib/NetworkManager/*.lease | wc -l')" = 1 ]
|
||||
check "no shell history" sh -c '! [ -e ~/.bash_history ]'
|
||||
check "the package cache is empty" [ -z "$(ls /var/cache/pacman/pkg)" ]
|
||||
root mkdir -p /run/top && root mount -o subvolid=5,ro "$(findmnt -no SOURCE / | sed 's/\[.*//')" /run/top
|
||||
check "the factory snapshot has no machine ID" sh -c "! printf '%s\n' '$PW' | sudo -S -p '' test -s /run/top/@factory/etc/machine-id"
|
||||
check "the factory snapshot holds no SSH login key" sh -c "! printf '%s\n' '$PW' | sudo -S -p '' sh -c 'ls /run/top/@factory/root/.ssh/authorized_keys /run/top/@factory/home/*/.ssh/authorized_keys' 2>/dev/null | grep -q ."
|
||||
check "the factory snapshot has no SSH host keys" sh -c "[ -z \"\$(printf '%s\n' '$PW' | sudo -S -p '' sh -c 'ls /run/top/@factory/etc/ssh/ssh_host_* 2>/dev/null')\" ]"
|
||||
check "no snapshots besides the current system" [ -z "$(root ls /.snapshots)" ]
|
||||
echo "omarchy $(pacman -Q omarchy 2>/dev/null | cut -d' ' -f2), kernel $(uname -r), $(systemd-analyze | head -1)"
|
||||
exit $fail
|
||||
GUEST
|
||||
Reference in new issue
Block a user