blob: e598700ae6dc9878d3b7b4fcbae59d889220ebb9 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
#!/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
live=$(readlink "$RELEASES/current" 2>/dev/null || true)
if [ "$live" != "$DEST_NAME" ]; then
rm -rf -- "$RELEASES/$DEST_NAME"
else
echo "kept $DEST_NAME: current still points at it" >> "$LOG"
fi
RC_LAST=1
fi
fi
if [ "$OK" -eq 0 ]; then
live=$(readlink "$RELEASES/current" 2>/dev/null || true)
if [ "$live" != "$DEST_NAME" ]; then
rm -rf -- "$RELEASES/$DEST_NAME"
else
echo "kept $DEST_NAME: current still points at it" >> "$LOG"
fi
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"
|