From 977579285b294e4958aa53a104e651e52c5ae653 Mon Sep 17 00:00:00 2001 From: Alex Maldonado Date: Thu, 15 Oct 2026 04:39:00 -0400 Subject: [PATCH] ci: run the check and site workflows on the linux-base runner Drop the hosted setup-pixi step for the self-hosted runner, install the locked environment in the site workflow, run the quick check before building, and serialize publishes. Co-Authored-By: Claude Fable 5.1 --- .gitea/workflows/check.yml | 5 +-- .gitea/workflows/site.yml | 77 +++++++++++++++++++++++++++++++------- deploy/README.md | 14 +++++-- 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index 3b4a4a1..d6096a4 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -9,13 +9,10 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [linux-base] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: prefix-dev/setup-pixi@v0.9.4 - with: - pixi-version: v0.69.0 - name: Check the repository run: pixi run check - name: Stress the reference implementation diff --git a/.gitea/workflows/site.yml b/.gitea/workflows/site.yml index 2fbbb1c..a48779e 100644 --- a/.gitea/workflows/site.yml +++ b/.gitea/workflows/site.yml @@ -1,33 +1,84 @@ -# Builds the website on every push to main and publishes it when the SITE_* secrets are set; see deploy/README.md. +# Builds and checks the website on every push to main, and publishes it when SITE_PATH is set; see deploy/README.md. name: site on: push: branches: [main] workflow_dispatch: +permissions: + contents: read +# One publish at a time, so two pushes cannot interleave their transfers; a later run waits instead of cancelling +# one halfway through. +concurrency: + group: site-publish + cancel-in-progress: false +defaults: + run: + shell: bash jobs: site: - runs-on: ubuntu-latest + runs-on: linux-base + timeout-minutes: 15 steps: - uses: actions/checkout@v4 - - uses: prefix-dev/setup-pixi@v0.9.4 with: - pixi-version: v0.69.0 + persist-credentials: false + # --locked fails when pixi.lock is out of date with pixi.toml instead of solving a new environment. + - name: Install the environment + run: pixi install --locked + # Everything but the byte-for-byte rebuild, including that every page, link and $id of the site resolves. + - name: Check the repository and the site + run: pixi run --frozen quick - name: Build the site - run: pixi run python tools/site.py --out _site + run: pixi run --frozen site --out _site - name: Publish the site + if: github.ref == 'refs/heads/main' env: SITE_HOST: ${{ secrets.SITE_HOST }} SITE_USER: ${{ secrets.SITE_USER }} SITE_PATH: ${{ secrets.SITE_PATH }} SITE_SSH_KEY: ${{ secrets.SITE_SSH_KEY }} + SITE_KNOWN_HOSTS: ${{ secrets.SITE_KNOWN_HOSTS }} run: | - if [ -z "$SITE_HOST" ]; then - echo "SITE_HOST is not set, so the site was built but not published." + if [ -z "$SITE_PATH" ]; then + echo "SITE_PATH is not set, so the site was built and checked but not published." exit 0 fi - command -v rsync >/dev/null || (sudo apt-get update && sudo apt-get install -y rsync) - mkdir -p ~/.ssh - printf '%s\n' "$SITE_SSH_KEY" > ~/.ssh/site_key - chmod 600 ~/.ssh/site_key - ssh-keyscan -H "$SITE_HOST" >> ~/.ssh/known_hosts - rsync -az --delete -e "ssh -i ~/.ssh/site_key" _site/ "$SITE_USER@$SITE_HOST:$SITE_PATH/" + # rsync --delete empties the folder it is given, so take only an absolute path at least two levels deep. + dest=${SITE_PATH%/} + case "$dest" in + /*/*) ;; + *) echo "SITE_PATH must be an absolute folder such as /srv/paperspec, not '$SITE_PATH'." >&2; exit 1 ;; + esac + case "$dest/" in + */../*|*/./*|*//*|*[[:space:]]*) echo "SITE_PATH must be a plain path, not '$SITE_PATH'." >&2; exit 1 ;; + esac + if [ ! -f _site/index.html ] || [ ! -f _site/404.html ]; then + echo "_site/ has no index.html or 404.html, so it was not published." >&2 + exit 1 + fi + command -v rsync >/dev/null || { echo "rsync is not installed on the runner." >&2; exit 1; } + # --checksum without --times leaves unchanged files untouched, so their Last-Modified and ETag stay valid + # in caches; --delay-updates and --delete-delay apply the changes together at the end of the transfer. + opts=(--recursive --links --perms --chmod=D755,F644 --checksum --delay-updates --delete-delay --stats) + if [ -z "$SITE_HOST" ]; then + # The runner is the web server. + if [ ! -d "$dest" ]; then + echo "$dest does not exist on the runner." >&2 + exit 1 + fi + rsync "${opts[@]}" _site/ "$dest/" + exit 0 + fi + if [ -z "$SITE_USER" ] || [ -z "$SITE_SSH_KEY" ] || [ -z "$SITE_KNOWN_HOSTS" ]; then + echo "Publishing over SSH needs SITE_USER, SITE_SSH_KEY and SITE_KNOWN_HOSTS as well as SITE_HOST." >&2 + exit 1 + fi + # The key and host keys live in a private folder that is removed however the step ends. + keys=$(mktemp -d) + trap 'rm -rf "$keys"' EXIT + printf '%s\n' "$SITE_SSH_KEY" > "$keys/id" + printf '%s\n' "$SITE_KNOWN_HOSTS" > "$keys/known_hosts" + chmod 600 "$keys/id" + ssh="ssh -i $keys/id -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes" + ssh="$ssh -o UserKnownHostsFile=$keys/known_hosts -o ConnectTimeout=20 -o ServerAliveInterval=15" + rsync "${opts[@]}" --compress -e "$ssh" _site/ "$SITE_USER@$SITE_HOST:$dest/" diff --git a/deploy/README.md b/deploy/README.md index cf67a22..d8819c6 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -41,16 +41,22 @@ Add it to the server's Caddy configuration, for example with an `import` line, a ## Publish from CI -The workflow `.gitea/workflows/site.yml` builds the site on every push to `main`, and publishes it with `rsync` over SSH when four repository secrets are set: +The workflow `.gitea/workflows/site.yml` runs on every push to `main`. +It installs the environment from `pixi.lock`, runs `pixi run quick`, builds the site, and publishes it with `rsync` when the repository secret `SITE_PATH` is set. +A failed check stops the publish, so the site never serves a release that does not pass its own checks. +Without `SITE_PATH` the workflow still builds and checks the site, and says that it did not publish. | Secret | Holds | | --- | --- | -| `SITE_HOST` | The server's host name. | +| `SITE_PATH` | The folder Caddy serves, such as `/srv/paperspec`: an absolute path at least two levels deep. | +| `SITE_HOST` | The server's host name. Leave it unset when the runner is the web server, and the site is copied to `SITE_PATH` on the runner. | | `SITE_USER` | The account that owns the site folder. | -| `SITE_PATH` | The folder Caddy serves, such as `/srv/paperspec`. | | `SITE_SSH_KEY` | A private SSH key whose public half is in that account's `authorized_keys`, and that can do nothing else. | +| `SITE_KNOWN_HOSTS` | The server's host keys, as lines of `known_hosts`. | -Without `SITE_HOST` the workflow still builds and checks the site, and says that it did not publish. +The last three are needed only with `SITE_HOST`. +Take `SITE_KNOWN_HOSTS` from `ssh-keyscan -H ` on a network you trust, and compare its fingerprints with `ssh-keygen -lf` on the server; the workflow refuses a server whose key does not match. +Copying only changed files keeps the `Last-Modified` and `ETag` of every other file, so caches stay valid across releases. ## Distributing packages