#!/bin/sh # Pull the newest passing build of the site from OxygenHub and make it # live (oxygenvcs-web#1). Run by a timer on the host that serves the site # (Earth now, a VisionFive 2 Lite later); the host needs no inbound # access and no credentials, since the repository is public. # # Each run: find the newest passing run of main, and if it is not the one # live already, download its site.tar.gz, check it against the run's # provenance statement (the artifact's SHA-256, this repository, main), # unpack it into releases/RUN and point `current` at it in one rename. # The last five releases stay for a rollback: `ln -sfn releases/N current`. # # Needs: sh, curl, python3 (reading JSON), sha256sum, tar. set -eu HUB=${HUB:-https://oxygenhub.io} REPO=${REPO:-andymccall/oxygenvcs-web} BOOKMARK=${BOOKMARK:-main} ROOT=${ROOT:-/srv/oxygenvcs} KEEP=${KEEP:-5} # Run after a new release goes live: busybox httpd resolves its document # root when it starts, so the service restarts it (see README.md). RELOAD=${RELOAD:-} mkdir -p "$ROOT/releases" cd "$ROOT" json() { python3 -I -c "import json,sys; $1" ; } run=$(curl -fsS "$HUB/api/v1/repos/$REPO/flow/runs?limit=20" | json " runs=[r for r in json.load(sys.stdin) if r['bookmark']=='$BOOKMARK' and r['status']=='passed'] print(runs[0]['number'] if runs else '')") [ -n "$run" ] || { echo "no passing run of $BOOKMARK yet"; exit 0; } live=$(readlink current 2>/dev/null | sed 's|releases/||' || true) [ "$run" = "$live" ] && exit 0 art="$HUB/$REPO/flow/$run/artifacts/site/site.tar.gz" tmp=$(mktemp -d "$ROOT/.pull.XXXXXX") trap 'rm -rf "$tmp"' EXIT curl -fsS "$art" -o "$tmp/site.tar.gz" curl -fsS "$art/provenance" -o "$tmp/provenance.json" got=$(sha256sum "$tmp/site.tar.gz" | cut -d' ' -f1) # The statement must name this file's digest, this repository and the # bookmark. (Its DSSE signature is the hub's; checking that here is for # when oh can verify provenance offline.) json " import base64 env=json.load(open('$tmp/provenance.json')) st=json.loads(base64.b64decode(env['payload'])) digests=[s['digest'].get('sha256') for s in st.get('subject',[])] p=st['predicate']['buildDefinition']['externalParameters'] assert '$got' in digests, 'the artifact does not match its provenance' assert p['repository']=='$HUB/$REPO', 'provenance names another repository: '+p['repository'] assert p['bookmark']=='$BOOKMARK', 'provenance names another bookmark: '+p['bookmark'] " <"$tmp/provenance.json" mkdir "$tmp/site" tar -C "$tmp/site" -xzf "$tmp/site.tar.gz" [ -f "$tmp/site/index.html" ] || { echo "run $run: no index.html in the site"; exit 1; } rm -rf "releases/$run" mv "$tmp/site" "releases/$run" ln -sfn "releases/$run" current.new mv -T current.new current echo "live: run $run ($got)" [ -z "$RELOAD" ] || sh -c "$RELOAD" # Keep the newest few. ls -1 releases | sort -n | head -n -"$KEEP" | while read -r old; do rm -rf "releases/$old"; done