2026-05-29 11:02:32 -06:00
|
|
|
#!/bin/bash
|
|
|
|
|
# Poll GitHub for a newly-published WROLPi release and build it.
|
|
|
|
|
#
|
|
|
|
|
# This is the *outbound* poller that replaces an inbound webhook: GitHub never
|
|
|
|
|
# connects to this machine. Once an hour (via the systemd timer) we ask the
|
|
|
|
|
# public GitHub API for the newest release, compare its version to whatever is
|
|
|
|
|
# already published in latest.json on the CDN, and if they differ we run
|
|
|
|
|
# release.sh to build and publish it. latest.json is the source of truth for
|
|
|
|
|
# "already built" -- there is no local bookkeeping for success.
|
|
|
|
|
#
|
2026-05-31 12:21:41 -06:00
|
|
|
# A local state file records a tag that failed to build. Builds are expensive
|
|
|
|
|
# (hours), so by default a failed tag is attempted once and then NOT auto-retried
|
|
|
|
|
# -- the notify hook alerts you, and you investigate and clear the marker (or run
|
|
|
|
|
# release.sh manually) instead of burning hours on blind retries.
|
2026-05-29 11:02:32 -06:00
|
|
|
#
|
|
|
|
|
# ./release-watch.sh # check once and act
|
|
|
|
|
#
|
|
|
|
|
# Honors $S3CFG (passed through to release.sh) and these optional env vars:
|
|
|
|
|
# STATE_DIR where to keep the lock + failure marker (default /var/lib/wrolpi-release)
|
2026-05-31 12:21:41 -06:00
|
|
|
# MAX_RETRIES build attempts before giving up on a tag (default 1 = no auto-retry)
|
2026-05-29 11:02:32 -06:00
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
|
|
|
|
|
|
|
|
|
REPO="lrnselfreliance/wrolpi"
|
2026-05-31 17:49:47 -06:00
|
|
|
# Read the manifest for the dedup from the Spaces ORIGIN, not the CDN. The CDN
|
|
|
|
|
# caches latest.json (~5 min), so a watcher run firing right after a release
|
|
|
|
|
# publishes can still read the previous tag and trigger a redundant rebuild.
|
|
|
|
|
# The origin is strongly consistent, so it always reflects the latest publish.
|
|
|
|
|
MANIFEST_URL="https://wrolpi.nyc3.digitaloceanspaces.com/latest.json"
|
2026-05-29 11:02:32 -06:00
|
|
|
STATE_DIR="${STATE_DIR:-/var/lib/wrolpi-release}"
|
2026-05-31 12:21:41 -06:00
|
|
|
MAX_RETRIES="${MAX_RETRIES:-1}"
|
2026-05-29 11:02:32 -06:00
|
|
|
|
|
|
|
|
LOCK="${STATE_DIR}/build.lock"
|
|
|
|
|
FAIL_STATE="${STATE_DIR}/failed-tag"
|
|
|
|
|
|
|
|
|
|
mkdir -p "${STATE_DIR}"
|
|
|
|
|
|
|
|
|
|
log() { echo "[release-watch] $*"; }
|
|
|
|
|
|
2026-05-30 14:11:09 -06:00
|
|
|
# Newest non-draft release tag. GitHub's /releases list is ordered by creation
|
|
|
|
|
# date, so [0] is the most recently *published* release -- exactly the trigger
|
|
|
|
|
# we want. Prereleases (-beta) are intentionally included (every WROLPi release
|
|
|
|
|
# is a prerelease), so we deliberately do NOT filter on `.prerelease`. Read-only
|
|
|
|
|
# and unauthenticated; the 60 req/hr public limit is irrelevant at hourly cadence.
|
2026-05-29 11:02:32 -06:00
|
|
|
LATEST_TAG=$(curl -fsSL -H "Accept: application/vnd.github+json" \
|
|
|
|
|
"https://api.github.com/repos/${REPO}/releases" \
|
|
|
|
|
| jq -r '[.[] | select(.draft == false)][0].tag_name // empty')
|
|
|
|
|
[ -n "${LATEST_TAG}" ] || { log "No releases found; nothing to do."; exit 0; }
|
|
|
|
|
|
2026-05-30 14:11:09 -06:00
|
|
|
# What is already built? release.sh records the git ref it built in the
|
|
|
|
|
# manifest's `.tag`. Compare that directly against the release tag: it is an
|
|
|
|
|
# exact match and immune to any divergence between a tag name and the
|
|
|
|
|
# version.txt content it carries. (Comparing versions could loop forever -- a
|
|
|
|
|
# build that succeeds but whose .version never equals the tag would rebuild
|
|
|
|
|
# every hour.) A missing manifest (404) means "nothing built yet".
|
2026-05-31 17:49:47 -06:00
|
|
|
PUBLISHED_TAG=$(curl -fsSL "${MANIFEST_URL}" 2>/dev/null \
|
2026-05-30 14:11:09 -06:00
|
|
|
| jq -r '.tag // empty' || true)
|
|
|
|
|
|
|
|
|
|
if [ "${PUBLISHED_TAG}" = "${LATEST_TAG}" ]; then
|
|
|
|
|
log "Up to date (published ${LATEST_TAG}); nothing to do."
|
2026-05-29 11:02:32 -06:00
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-30 14:11:09 -06:00
|
|
|
log "New release ${LATEST_TAG} (published: ${PUBLISHED_TAG:-none})."
|
2026-05-29 11:02:32 -06:00
|
|
|
|
|
|
|
|
# Skip a tag that has already failed MAX_RETRIES times -- needs a human.
|
|
|
|
|
if [ -f "${FAIL_STATE}" ]; then
|
|
|
|
|
read -r failed_tag failed_count < "${FAIL_STATE}" || true
|
|
|
|
|
if [ "${failed_tag}" = "${LATEST_TAG}" ] && [ "${failed_count:-0}" -ge "${MAX_RETRIES}" ]; then
|
|
|
|
|
log "ERROR: ${LATEST_TAG} has failed ${failed_count} times; not auto-retrying."
|
|
|
|
|
log "Investigate, then run release.sh manually or remove ${FAIL_STATE}."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Only one build at a time. If the timer fires mid-build, just exit.
|
|
|
|
|
exec 9>"${LOCK}"
|
|
|
|
|
if ! flock -n 9; then
|
|
|
|
|
log "A build is already running; skipping this check."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
log "Building ${LATEST_TAG}..."
|
|
|
|
|
if "${SCRIPT_DIR}/release.sh" -b "${LATEST_TAG}"; then
|
|
|
|
|
log "Published ${LATEST_TAG}."
|
|
|
|
|
rm -f "${FAIL_STATE}"
|
|
|
|
|
else
|
|
|
|
|
rc=$?
|
|
|
|
|
count=1
|
|
|
|
|
if [ -f "${FAIL_STATE}" ]; then
|
|
|
|
|
read -r ft fc < "${FAIL_STATE}" || true
|
|
|
|
|
[ "${ft}" = "${LATEST_TAG}" ] && count=$(( ${fc:-0} + 1 ))
|
|
|
|
|
fi
|
|
|
|
|
echo "${LATEST_TAG} ${count}" > "${FAIL_STATE}"
|
|
|
|
|
log "ERROR: build of ${LATEST_TAG} failed (attempt ${count}, exit ${rc})."
|
|
|
|
|
exit "${rc}"
|
|
|
|
|
fi
|