Skip to content

Publish image

Publish image #15

Workflow file for this run

name: Publish image
# Daily, because an installation without a network installs the age of the
# image it was made from, and nothing about publishing one is expensive now
# that what is published is only what the end-to-end cases passed. Also on
# request, for the day when something has to be looked at before tomorrow.
#
# At an unremarkable minute of the middle of the day, and not at the top of an
# hour, because a schedule here is a request rather than an appointment: it is
# run when there is room to run it, and the top of every hour is where everyone
# else asked first. Nothing waits on this, so a late one costs nothing and a
# skipped one is a day without a new image.
on:
schedule:
- cron: '37 12 * * *'
workflow_dispatch:
# Two of these would be building the same thing and uploading to the same
# place, and the second to finish would decide what the day's image is.
concurrency:
group: publish-image
cancel-in-progress: false
jobs:
image:
runs-on: ubuntu-latest
# mkarchiso makes filesystems and mounts them, which a container cannot do
# without being allowed to.
container:
image: archlinux:latest
options: --privileged
# Its own, and not the one the signing key is in. Nothing here signs
# anything, so nothing here has any business reaching that key.
environment: image
steps:
- name: Install what building an image needs
run: |
pacman -Syu --noconfirm --needed archiso git curl aws-cli-v2 jq \
qemu-system-x86 qemu-img edk2-ovmf libarchive util-linux procps-ng
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# The keyring has to be there before anything is verified against it, and
# a container starts without one.
- name: Start this machine's keyring
run: |
set -eu
pacman-key --init
pacman-key --populate archlinux
- name: Build the image
run: |
set -eu
mkdir -p out
archiso/build.sh "$PWD/out"
ls -lh out
# Before it goes anywhere. Every image published until now became the one
# people download with nothing having booted it, and four of the six built
# on 2026-09-11 were broken in ways only booting them showed. A failure
# here leaves the image unuploaded, which is the whole point of running it
# here rather than after.
#
# The cases boot the image under QEMU, so this wants hardware
# acceleration; the harness says which it got and goes on either way. What
# each case is and what it holds the image to is
# docs/development/006-end-to-end-testing.md.
- name: Test the image before publishing it
run: |
set -eu
tests/e2e/run.sh --transcripts "$PWD/transcripts" "$(ls out/*.iso)"
# The harness reports that a case failed; what it failed at is in the
# console of the guest that failed it, and that is written on a machine
# which stops existing when this run ends. So it is taken before it goes,
# and only when there is a reason to: a run where everything passed has
# nothing in them anybody wants.
- name: Keep what a failed case left behind
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: end-to-end-transcripts
path: transcripts/
# A bucket of its own and not the one the packages are in. They are read
# by different things at different rates, and this one has images
# deleted out of it, which is not something to do next to a repository
# whose whole property is that nothing in it disappears.
- name: Upload it, and keep the last two
env:
# These names are the same ones the package workflow reads, and they
# are not the same secrets: what they resolve to is decided by the
# environment this job declares. The credentials behind them here are
# scoped to this bucket alone, and the ones the packages are
# published with would not write to it.
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
run: |
set -eu
image="$(ls out/*.iso)"
name="$(basename "$image")"
# Its checksum goes up beside it, because what the project says about
# an image is how someone tells they have the one it published.
( cd out && sha256sum "$name" > "$name.sha256" )
endpoint="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
for file in "$image" "$image.sha256"; do
aws s3 cp --endpoint-url "$endpoint" \
"$file" "s3://${R2_BUCKET}/$(basename "$file")"
done
# Pruned after the new one is up and not before: an upload that fails
# then leaves the two that were there rather than one. It costs a few
# minutes with three of them in the bucket, which is what that is
# worth.
#
# And the one this replaces is left alone, for the reason
# docs/decisions/018-installation-iso.md gives: somebody may be half
# way through downloading it.
#
# Images are named for the minute they were built, so what sorts last
# is what was built last, and nothing has to read a date out of
# anything.
#
# By bytes, which is what LC_ALL=C asks for. The Worker that answers
# with the newest image sorts the same keys in JavaScript, which
# orders by code unit and has no locale to take; a sort here that
# took one would weigh punctuation differently and the two would
# disagree about which image is the current one.
aws s3api list-objects-v2 --endpoint-url "$endpoint" \
--bucket "$R2_BUCKET" --query 'Contents[].Key' --output json |
jq -r '.[] | select(endswith(".iso"))' | LC_ALL=C sort | head -n -2 |
while read -r old; do
echo "removing $old"
aws s3 rm --endpoint-url "$endpoint" "s3://${R2_BUCKET}/$old"
aws s3 rm --endpoint-url "$endpoint" "s3://${R2_BUCKET}/$old.sha256"
done