# 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: linux-base timeout-minutes: 15 steps: - uses: actions/checkout@v4 with: 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 --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_PATH" ]; then echo "SITE_PATH is not set, so the site was built and checked but not published." exit 0 fi # 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/"