diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 8e78109..320f604 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -43,12 +43,8 @@ env: SITE: https://nightly.omarchy.org jobs: - # Which channels this run makes, under which version, and where each goes. - # An edge version is the date and the run number. A stable version is the - # release's own, and a release already published is left out of a scheduled - # run. Two builds of one release are not the same bytes, so a stable image - # also has the run number in its path: a published path is never reused, and - # a publish that stopped halfway is simply made again. + # 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 @@ -56,27 +52,17 @@ jobs: outputs: channels: ${{ steps.plan.outputs.channels }} steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - id: plan env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - # A run that is started again makes its images again, and they are - # not the same bytes: it gets its own number too. - build=${{ github.run_number }} - [[ ${{ github.run_attempt }} == 1 ]] || build+=.${{ github.run_attempt }} - edge=$(date -u +%Y.%m.%d).$build - stable=$(gh api repos/omacom/omarchy/releases/latest --jq .tag_name) - stable=${stable#v} - [[ $stable =~ ^[0-9]+(\.[0-9]+)+$ ]] || { echo "::error::omacom/omarchy's latest release is '$stable', not a version"; exit 1; } + 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=$(jq -cn --arg edge "$edge" '[{channel: "edge", version: $edge, release: "edge/\($edge)"}]') - if [[ $GITHUB_EVENT_NAME == schedule && $published == "$stable" ]]; then - echo "stable $stable is already published" - else - channels=$(jq -c --arg stable "$stable" --arg run "$build" \ - '. + [{channel: "stable", version: $stable, release: "stable/\($stable)/\($run)"}]' <<<"$channels") - fi + 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: @@ -184,47 +170,12 @@ jobs: 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 - mkdir -p "$dir/vm" - # The edge ISO is published here. The released one already has its - # place, so latest.json points there and it is not uploaded again. - if [[ $CHANNEL == edge ]]; then - iso=omarchy-$CHANNEL-$VERSION.iso iso_url=$SITE/$RELEASE/$iso - mv release/*.iso "$dir/$iso" - (cd "$dir" && sha256sum "$iso" >"$iso.sha256") - iso_sha256=$(cut -d" " -f1 "$dir/$iso.sha256") iso_size=$(stat -c %s "$dir/$iso") - else - iso=omarchy-$VERSION.iso iso_url=https://iso.omarchy.org/$iso - iso_sha256=$(sha256sum "release/$iso" | cut -d" " -f1) iso_size=$(stat -c %s "release/$iso") - fi - mv sealed/* "$dir/vm/" - cp test/vm-image/vm-boot "$dir/vm/vm-boot" - (cd "$dir/vm" && sha256sum base.qcow2 OVMF_VARS.4m.fd vmlinuz initrd cmdline vm-boot >SHA256SUMS) - - omarchy=$(awk '$1 == "omarchy" || $1 == "omarchy-dev" { print $2; exit }' vm-packages.txt) - jq -n \ - --arg version "$VERSION" --arg channel "$CHANNEL" --arg site "$SITE/$RELEASE" --arg iso_url "$iso_url" \ - --arg built_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - --arg sha256 "$iso_sha256" --argjson size "$iso_size" \ - --arg iso_commit "$GITHUB_SHA" \ - --arg omarchy_version "$omarchy" --arg omarchy_commit "$(sed -nE "s/.*\.g([0-9a-f]+)-.*/\1/p" <<<"$omarchy")" \ - --arg kernel "$(tail -n 1 vm-packages.txt)" \ - --arg disk_sha256 "$(awk '$2 == "base.qcow2" { print $1 }' "$dir/vm/SHA256SUMS")" \ - --argjson disk_size "$(stat -c %s "$dir/vm/base.qcow2")" \ - '{ - version: $version, channel: $channel, built_at: $built_at, - url: $iso_url, sha256: $sha256, size: $size, - iso_commit: $iso_commit, omarchy_version: $omarchy_version, omarchy_commit: $omarchy_commit, - kernel: $kernel, - vm: { - url: "\($site)/vm/", disk: "\($site)/vm/base.qcow2", disk_sha256: $disk_sha256, disk_size: $disk_size, - files: ["base.qcow2", "OVMF_VARS.4m.fd", "vmlinuz", "initrd", "cmdline", "vm-boot", "SHA256SUMS"], - user: "omarchy", password: "omarchy", - ssh: "keys only; vm-boot makes one for each boot" - } - }' | tee "out/$CHANNEL/latest.json" { echo "## Nightly $CHANNEL $VERSION" diff --git a/test/integration.d/base-test.sh b/test/integration.d/base-test.sh index 0b5e157..e7fcc9f 100644 --- a/test/integration.d/base-test.sh +++ b/test/integration.d/base-test.sh @@ -514,6 +514,12 @@ detect_packages() { 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)" @@ -576,11 +582,10 @@ install_phase() { printf -v progress_name 'success-install-progress-%04ds' "$waited" capture_console "$progress_name" fi - # The firmware names what it starts on the serial port. Once that is the - # installed disk's Limine, 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 ]] && grep -aq 'starting Boot.*"Limine"' "$RUN_DIR/install-serial.log" 2>/dev/null; then + # 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 diff --git a/test/unit/nightly-test.sh b/test/unit/nightly-test.sh new file mode 100644 index 0000000..2c9a91d --- /dev/null +++ b/test/unit/nightly-test.sh @@ -0,0 +1,152 @@ +#!/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 OVMF_VARS.4m.fd 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/vm/OVMF_VARS.4m.fd ./edge/2026.10.09.431/vm/SHA256SUMS ./edge/2026.10.09.431/vm/base.qcow2 ./edge/2026.10.09.431/vm/cmdline ./edge/2026.10.09.431/vm/initrd ./edge/2026.10.09.431/vm/vm-boot ./edge/2026.10.09.431/vm/vmlinuz ./edge/latest.json" +(cd "$dir" && sha256sum -c --quiet ./*.sha256) || fail "edge: the ISO's checksum file does not match the ISO" +(cd "$dir/vm" && sha256sum -c --quiet SHA256SUMS) || fail "edge: SHA256SUMS does not match the image" +equal "edge: SHA256SUMS covers every file beside it" \ + "$(awk '{ print $2 }' "$dir/vm/SHA256SUMS" | sort | paste -sd' ')" "$(cd "$dir/vm" && ls | grep -v SHA256SUMS | sort | paste -sd' ')" +cmp -s "$dir/vm/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.disk)" "https://nightly.example/edge/2026.10.09.431/vm/base.qcow2" +equal "edge: image checksum is the disk's" "$(field .vm.disk_sha256)" "$(sha256sum "$dir/vm/base.qcow2" | cut -d' ' -f1)" +equal "edge: every listed file is published" "$(field '.vm.files[]' | sort | paste -sd' ')" "$(cd "$dir/vm" && 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.disk)" "https://nightly.example/stable/4.0.4/431/vm/base.qcow2" +(cd "$tmp/out/stable/4.0.4/431/vm" && sha256sum -c --quiet SHA256SUMS) || fail "stable: SHA256SUMS does not match the image" + +# 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" diff --git a/test/vm-image/nightly-layout b/test/vm-image/nightly-layout new file mode 100755 index 0000000..c58f60c --- /dev/null +++ b/test/vm-image/nightly-layout @@ -0,0 +1,98 @@ +#!/bin/bash +# Lays out what one channel of a nightly run publishes and writes the +# latest.json that describes it. +# +# nightly-layout +# +# 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 /vm/ with the image, its checksums and vm-boot, and +# /latest.json. The edge ISO is published too, next to the image. +# The released one 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,13p' "$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" +files=(base.qcow2 OVMF_VARS.4m.fd vmlinuz initrd cmdline) +for file in "${files[@]}"; 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/vm" "$out/$channel" +if [[ $channel == edge ]]; then + iso=omarchy-$channel-$version.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 + iso=omarchy-$version.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 + +for file in "${files[@]}"; do mv "$sealed/$file" "$dir/vm/$file"; done +cp "$here/vm-boot" "$dir/vm/vm-boot" +files+=(vm-boot) +(cd "$dir/vm" && sha256sum "${files[@]}" >SHA256SUMS) + +latest=$out/$channel/latest.json +jq -n \ + --arg version "$version" --arg channel "$channel" --arg vm "$site/$release/vm" \ + --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 disk_sha256 "$(awk '$2 == "base.qcow2" { print $1 }' "$dir/vm/SHA256SUMS")" \ + --argjson disk_size "$(stat -c %s "$dir/vm/base.qcow2")" \ + --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: "\($vm)/", disk: "\($vm)/base.qcow2", disk_sha256: $disk_sha256, disk_size: $disk_size, + files: ($ARGS.positional + ["SHA256SUMS"]), + user: "omarchy", password: "omarchy", + ssh: "keys only; vm-boot makes one for each boot" + } + }' "${files[@]}" >"$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.disk | test("^https://[^\n ]+/vm/base\\.qcow2$")) and (.vm.disk_sha256 | sha) and .vm.disk_size > 0 +' "$latest" >/dev/null || { cat "$latest" >&2; fail "latest.json does not say what it should"; } +cat "$latest" diff --git a/test/vm-image/nightly-plan b/test/vm-image/nightly-plan new file mode 100755 index 0000000..5738781 --- /dev/null +++ b/test/vm-image/nightly-plan @@ -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 [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"