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:
co-authored by
Claude Opus 5
parent
56114b727c
commit
95c073d4c2
@@ -66,3 +66,8 @@ scripts/.last-check
|
|||||||
|
|
||||||
# The deploy template is meant to be read, unlike a real .env
|
# The deploy template is meant to be read, unlike a real .env
|
||||||
!deploy/.env.example
|
!deploy/.env.example
|
||||||
|
|
||||||
|
# publish bookkeeping
|
||||||
|
deploy/publish.log
|
||||||
|
deploy/.last-check
|
||||||
|
deploy/.publish.lock
|
||||||
|
|||||||
+24
-3
@@ -66,13 +66,34 @@ second copy. The A record has to point at the droplet first.
|
|||||||
|
|
||||||
## Publishing a release
|
## Publishing a release
|
||||||
|
|
||||||
|
Automatic. `autofirmer-publish.timer` fetches every minute and publishes when
|
||||||
|
master moves — the same shape as the autotrader stack's auto-deploy next door.
|
||||||
|
Install it once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/install-autopublish.sh # as root
|
||||||
|
```
|
||||||
|
|
||||||
|
Only changes under `extension/` or `deploy/` trigger a publish; anything else is
|
||||||
|
pulled and left alone, because repacking for a README edit would republish a
|
||||||
|
byte-identical CRX every minute.
|
||||||
|
|
||||||
|
**Bump `version` in `extension/manifest.json` for every release.** Chrome ignores
|
||||||
|
a CRX that is not strictly newer than the copy it has, so republishing at the
|
||||||
|
same version is a silent no-op — the kind discovered days later, when a fix
|
||||||
|
everyone believed had shipped turns out never to have left the building.
|
||||||
|
`auto-publish.sh` refuses that case outright and logs why, rather than letting
|
||||||
|
it pass quietly.
|
||||||
|
|
||||||
|
By hand, any time:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull && ./deploy/deploy.sh
|
git pull && ./deploy/deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
`deploy.sh` packs whatever is in `extension/` at the version its `manifest.json`
|
Watch it with `tail -f deploy/publish.log`, and `cat deploy/.last-check` for the
|
||||||
declares, so **bump that version or nothing updates** — Chrome compares versions
|
heartbeat — that file is rewritten on every run, whether or not anything came
|
||||||
and ignores a CRX that is not newer than what it has.
|
down, which is the only way to tell "running, nothing to do" from "not running".
|
||||||
|
|
||||||
The browsers pick it up on Chrome's own schedule, roughly every five hours. They
|
The browsers pick it up on Chrome's own schedule, roughly every five hours. They
|
||||||
also pick it up within seconds if the dashboard is running, because the
|
also pick it up within seconds if the dashboard is running, because the
|
||||||
|
|||||||
Executable
+101
@@ -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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Publish a new AutoFirmer CRX when Gitea's master branch moves
|
||||||
|
After=docker.service
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=/opt/autofirmer
|
||||||
|
ExecStart=/opt/autofirmer/deploy/auto-publish.sh
|
||||||
|
# Packing pulls a node image on a cold cache; well inside this.
|
||||||
|
TimeoutStartSec=1800
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Check Gitea for AutoFirmer extension releases every minute
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=60s
|
||||||
|
AccuracySec=5s
|
||||||
|
Unit=autofirmer-publish.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install the systemd timer that publishes a new CRX when master moves.
|
||||||
|
# Run as root on the droplet: ./deploy/install-autopublish.sh
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
[ "$(id -u)" -eq 0 ] || { echo "run as root" >&2; exit 1; }
|
||||||
|
|
||||||
|
install -m 644 deploy/autofirmer-publish.service /etc/systemd/system/
|
||||||
|
install -m 644 deploy/autofirmer-publish.timer /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now autofirmer-publish.timer
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " installed. next run:"
|
||||||
|
systemctl list-timers autofirmer-publish.timer --no-pager | sed -n '2p'
|
||||||
|
echo
|
||||||
|
echo " logs: tail -f /opt/autofirmer/deploy/publish.log"
|
||||||
|
echo " heartbeat: cat /opt/autofirmer/deploy/.last-check"
|
||||||
|
echo " disable: systemctl disable --now autofirmer-publish.timer"
|
||||||
Reference in New Issue
Block a user