diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-19 19:42:42 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-19 19:42:42 +0800 |
| commit | 9d71d06a1f3ee294634bf45373bbf1944bf03caf (patch) | |
| tree | e2f11ba0461615d51ea9e5469d0fd0038ba0e971 /deploy/scripts/build.sh | |
| parent | ead9932988f519932af39767ae09cf55081bd17f (diff) | |
| download | blog-9d71d06a1f3ee294634bf45373bbf1944bf03caf.tar.gz | |
chore: add self-hosted deploy pipeline (blog-deploy)
Diffstat (limited to 'deploy/scripts/build.sh')
| -rwxr-xr-x | deploy/scripts/build.sh | 153 |
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" |
