Publish the CRX from a timer, and refuse an unbumped version

Mirrors the autotrader stack's auto-deploy: fetch every minute, act when master
moves, lock against overlap, rewrite .last-check every run so a silent failure
is visible. Only extension/ and deploy/ changes publish - repacking for a docs
edit would republish an identical CRX every minute.

The guard is the part that earns its place. Chrome ignores a CRX that is not
strictly newer, so shipping extension changes without bumping manifest.json is
a no-op that looks exactly like success until someone notices the fix never
arrived. That now fails loudly instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-31 16:10:21 -05:00
co-authored by Claude Opus 5
parent 56114b727c
commit 95c073d4c2
6 changed files with 171 additions and 3 deletions
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
#
# Publish a new CRX when master moves. Run by a systemd timer.
#
# Bash rather than Node, matching the autotrader stack next door: nothing on
# this host runs outside Docker, so there is no Node to depend on.
#
# Only a change under extension/ or deploy/ is worth acting on — the dashboard
# and the clicker are pulled by each instance itself, and repacking for a README
# edit would publish a byte-identical CRX every few minutes.
#
# ./auto-publish.sh # normal
# ./auto-publish.sh --dry-run # report only
set -euo pipefail
cd "$(dirname "$0")/.."
ROOT="$(pwd)"
BRANCH="${AUTOPUBLISH_BRANCH:-master}"
LOG="$ROOT/deploy/publish.log"
LAST_CHECK="$ROOT/deploy/.last-check"
LOCK="$ROOT/deploy/.publish.lock"
PUBLISHED_XML="${UPDATES_DIR:-/opt/autofirmer-updates}/public/updates.xml"
STALE_LOCK_SECS=3600
LOG_MAX_BYTES=262144
LOG_KEEP_LINES=500
DRY_RUN=0
[ "${1:-}" = "--dry-run" ] && DRY_RUN=1
log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" | tee -a "$LOG"; }
record() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" > "$LAST_CHECK" 2>/dev/null || true; }
# Trimmed once per run, before anything is written: the no-change path writes
# nothing, so this is the only moment the file can have grown.
if [ -f "$LOG" ] && [ "$(stat -c%s "$LOG")" -gt "$LOG_MAX_BYTES" ]; then
tail -n "$LOG_KEEP_LINES" "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
fi
if [ "$DRY_RUN" -eq 0 ] && [ -f "$LOCK" ]; then
LOCK_AGE=$(( $(date +%s) - $(cat "$LOCK" 2>/dev/null || echo 0) ))
if [ "$LOCK_AGE" -lt "$STALE_LOCK_SECS" ]; then
exit 0 # already publishing; say nothing
fi
log "clearing a stale lock (${LOCK_AGE}s old)"
fi
[ "$DRY_RUN" -eq 0 ] && date +%s > "$LOCK"
OUTCOME="interrupted"
cleanup() { [ "$DRY_RUN" -eq 0 ] && { rm -f "$LOCK"; record "$OUTCOME"; }; }
trap cleanup EXIT
git fetch origin "$BRANCH" --quiet
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse "origin/$BRANCH")
if [ "$LOCAL" = "$REMOTE" ]; then
OUTCOME="up to date at ${LOCAL:0:8}"
exit 0 # the common path: silent
fi
log "update available: ${LOCAL:0:8} -> ${REMOTE:0:8}"
CHANGED=$(git diff --name-only "$LOCAL" "$REMOTE")
TOUCHES_RELEASE=0
echo "$CHANGED" | grep -qE '^(extension|deploy)/' && TOUCHES_RELEASE=1
if [ "$DRY_RUN" -eq 1 ]; then
log "dry run - would pull; release-relevant changes: $TOUCHES_RELEASE"
exit 0
fi
git pull --ff-only origin "$BRANCH" --quiet
if [ "$TOUCHES_RELEASE" -eq 0 ]; then
# Pulled so the checkout stays current, but nothing that ships changed.
OUTCOME="pulled ${REMOTE:0:8}, nothing to publish"
log "$OUTCOME"
exit 0
fi
# The guard worth having. Chrome ignores a CRX that is not strictly newer than
# what it already has, so republishing at the same version is a silent no-op —
# the kind you discover days later, when a fix everyone believed had shipped
# turns out never to have left the building.
VERSION=$(python3 -c 'import json;print(json.load(open("extension/manifest.json"))["version"])')
LIVE=$(sed -n 's/.*[^a-z]version="\([0-9][^"]*\)".*/\1/p' "$PUBLISHED_XML" 2>/dev/null | head -1)
if echo "$CHANGED" | grep -qE '^extension/' && [ "$VERSION" = "$LIVE" ]; then
log "REFUSED: extension/ changed but manifest.json is still $VERSION, which is"
log " already published. Chrome would ignore the new CRX. Bump the"
log " version and push again."
OUTCOME="refused - version $VERSION not bumped"
exit 1
fi
if ./deploy/deploy.sh >> "$LOG" 2>&1; then
OUTCOME="published $VERSION at ${REMOTE:0:8}"
log "$OUTCOME"
else
log "ABORTED: deploy.sh failed - the previous CRX is still being served"
OUTCOME="publish failed - not switched"
exit 1
fi