summaryrefslogtreecommitdiff
path: root/deploy/scripts/build.sh
diff options
context:
space:
mode:
Diffstat (limited to 'deploy/scripts/build.sh')
-rwxr-xr-xdeploy/scripts/build.sh153
1 files changed, 153 insertions, 0 deletions
diff --git a/deploy/scripts/build.sh b/deploy/scripts/build.sh
new file mode 100755
index 00000000..b76d6ec0
--- /dev/null
+++ b/deploy/scripts/build.sh
@@ -0,0 +1,153 @@
+#!/usr/bin/env bash
+# Blog deploy: build Hexo from blog.git main, verify strictly, publish atomically.
+# Triggered by systemd user path unit watching /home/somhairle/git/blog.git/deploy.signal
+# (signal written by the post-receive hook inside the gitweb container at $GIT_DIR/deploy.signal).
+# Failure policy: any failed step aborts before the current symlink is touched;
+# the previous release always stays live.
+set -uo pipefail
+
+export PATH="/home/somhairle/.hermes/node/bin:$PATH"
+DEPLOY=/home/somhairle/blog-deploy
+GITDIR=/home/somhairle/git/blog.git
+SIGNAL="$GITDIR/deploy.signal"
+SRC="$DEPLOY/work/src"
+RELEASES="$DEPLOY/releases"
+LOGDIR="$DEPLOY/logs"
+LOCK="$DEPLOY/work/build.lock"
+STATE="$DEPLOY/work/last-built.signal"
+PREV="$DEPLOY/work/current.prev"
+KEEP=5
+MAX_LOOP=5
+
+mkdir -p "$LOGDIR" "$RELEASES" "$DEPLOY/work"
+
+exec 9>"$LOCK"
+if ! flock -n 9; then
+ echo "$(date -u '+%FT%TZ') build already running; extra trigger coalesced" >> "$LOGDIR/build.log"
+ exit 0
+fi
+
+read_signal() { if [ -f "$SIGNAL" ]; then cat "$SIGNAL"; else echo none; fi; }
+
+RC_LAST=0
+n=0
+while :; do
+ n=$((n + 1))
+ SIG_START=$(read_signal)
+ TS=$(date -u +%Y%m%dT%H%M%SZ)
+ LOG="$LOGDIR/build-$TS.log"
+ RC_LAST=0
+ (
+ echo "==== build start $TS (signal: $SIG_START) ===="
+ if [ -d "$SRC/.git" ]; then
+ git -C "$SRC" fetch --quiet "$GITDIR" main || { echo "FAILED: git fetch"; exit 1; }
+ git -C "$SRC" reset --hard --quiet FETCH_HEAD || { echo "FAILED: git reset"; exit 1; }
+ git -C "$SRC" clean -fdxq -e node_modules || { echo "FAILED: git clean"; exit 1; }
+ else
+ git clone --quiet "$GITDIR" "$SRC" || { echo "FAILED: git clone"; exit 1; }
+ git -C "$SRC" checkout --quiet main || { echo "FAILED: git checkout"; exit 1; }
+ fi
+ SHA=$(git -C "$SRC" rev-parse HEAD) || { echo "FAILED: rev-parse"; exit 1; }
+ echo "building commit $SHA"
+ cd "$SRC" || { echo "FAILED: cd src"; exit 1; }
+ npm install --no-save --no-audit --no-fund || { echo "FAILED: npm install"; exit 1; }
+ npx hexo clean || { echo "FAILED: hexo clean"; exit 1; }
+ npx hexo generate || { echo "FAILED: hexo generate"; exit 1; }
+ ) >> "$LOG" 2>&1
+ RC_LAST=$?
+
+ # gate 1: hexo can exit 0 while logging errors -> scan the log
+ if [ "$RC_LAST" -eq 0 ]; then
+ if grep -nE 'ERROR|FATAL|CoercionError' "$LOG" >/dev/null 2>&1; then
+ echo "==== build $TS REJECTED: errors found in build log ====" >> "$LOG"
+ grep -nE 'ERROR|FATAL|CoercionError' "$LOG" | head -20 >> "$LOG"
+ RC_LAST=1
+ fi
+ fi
+
+ # gate 2: required artifacts (index, css, latest diary page, moon photo)
+ if [ "$RC_LAST" -eq 0 ]; then
+ if [ ! -s "$SRC/public/index.html" ] \
+ || ! find "$SRC/public" -name '*.css' -print -quit 2>/dev/null | grep -q . \
+ || ! find "$SRC/public/2026/09/19" -name index.html -print -quit 2>/dev/null | grep -q . \
+ || [ ! -s "$SRC/public/img/日记/二〇二六年九月十九日.jpeg" ]; then
+ echo "==== build $TS REJECTED: required artifacts missing ====" >> "$LOG"
+ RC_LAST=1
+ fi
+ fi
+
+ # publish: every step checked; only switch current after all copies succeed
+ if [ "$RC_LAST" -eq 0 ]; then
+ SHORT=$(git -C "$SRC" rev-parse --short HEAD 2>/dev/null) || SHORT=""
+ if [ -z "$SHORT" ]; then
+ echo "==== build $TS REJECTED: cannot determine short sha ====" >> "$LOG"
+ RC_LAST=1
+ else
+ DEST_NAME="v-$TS-$SHORT"
+ OK=1
+ readlink "$RELEASES/current" > "$PREV" 2>/dev/null || true
+ mkdir -p "$RELEASES/$DEST_NAME" || { echo "FAILED: mkdir $DEST_NAME" >> "$LOG"; OK=0; }
+ if [ "$OK" -eq 1 ]; then
+ rsync -a "$SRC/public/" "$RELEASES/$DEST_NAME/" || { echo "FAILED: rsync" >> "$LOG"; OK=0; }
+ fi
+ if [ "$OK" -eq 1 ]; then
+ chmod -R a+rX "$RELEASES/$DEST_NAME" || { echo "FAILED: chmod" >> "$LOG"; OK=0; }
+ fi
+ if [ "$OK" -eq 1 ]; then
+ ln -sfn "$DEST_NAME" "$RELEASES/.current.tmp" || { echo "FAILED: ln" >> "$LOG"; OK=0; }
+ fi
+ if [ "$OK" -eq 1 ]; then
+ mv -Tf "$RELEASES/.current.tmp" "$RELEASES/current" || { echo "FAILED: mv" >> "$LOG"; OK=0; }
+ fi
+ if [ "$OK" -eq 1 ]; then
+ # gate 3: sanity through the (relative) symlink; rollback within same lock if broken
+ if [ -s "$RELEASES/current/index.html" ] && [ -s "$RELEASES/current/img/日记/二〇二六年九月十九日.jpeg" ]; then
+ echo "published $DEST_NAME (current -> $DEST_NAME)"
+ printf '%s\n' "$SIG_START" > "$STATE" || true
+ else
+ echo "==== build $TS REJECTED after switch, rolling back ====" >> "$LOG"
+ prev=$(cat "$PREV" 2>/dev/null || true)
+ if [ -n "$prev" ]; then
+ ln -sfn "$prev" "$RELEASES/.current.tmp" \
+ && mv -Tf "$RELEASES/.current.tmp" "$RELEASES/current" \
+ && echo "rolled back to $prev" >> "$LOG" \
+ || echo "ROLLBACK FAILED: current -> $DEST_NAME stays" >> "$LOG"
+ fi
+ rm -rf -- "$RELEASES/$DEST_NAME"
+ RC_LAST=1
+ fi
+ fi
+ if [ "$OK" -eq 0 ]; then
+ rm -rf -- "$RELEASES/$DEST_NAME"
+ echo "==== build $TS FAILED at publish, previous release kept ====" >> "$LOG"
+ RC_LAST=1
+ fi
+ fi
+ fi
+
+ if [ "$RC_LAST" -ne 0 ]; then
+ echo "==== build $TS FAILED, previous release kept ====" >> "$LOG"
+ fi
+
+ # consume signals queued while we were building; never drop a push
+ SIG_END=$(read_signal)
+ if [ "$SIG_END" != "$SIG_START" ]; then
+ if [ "$n" -ge "$MAX_LOOP" ]; then
+ echo "==== queued signals exceed MAX_LOOP=$MAX_LOOP, deferring to next trigger ====" >> "$LOG"
+ break
+ fi
+ echo "==== new signal queued during build, rebuilding ====" >> "$LOG"
+ continue
+ fi
+ break
+done
+
+ls -1t "$LOGDIR"/build-*.log 2>/dev/null | tail -n +21 | xargs -r rm -f
+ls -1dt "$RELEASES"/v-* 2>/dev/null | tail -n +"$((KEEP + 1))" | while IFS= read -r old; do
+ live=$(readlink -f "$RELEASES/current" 2>/dev/null || true)
+ if [ "$live" != "$(readlink -f "$old")" ]; then
+ rm -rf -- "$old"
+ echo "pruned $old" >> "$LOGDIR/build.log"
+ fi
+done
+exit "$RC_LAST"