summaryrefslogtreecommitdiff
path: root/deploy/scripts/build.sh
blob: b76d6ec0c70b9f4d907b27de0e92275240fd16c3 (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
#!/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"