summaryrefslogtreecommitdiff
path: root/deploy
diff options
context:
space:
mode:
Diffstat (limited to 'deploy')
-rw-r--r--deploy/docker-compose.yml11
-rwxr-xr-xdeploy/git-hook/post-receive21
-rw-r--r--deploy/nginx-blog.conf14
-rwxr-xr-xdeploy/scripts/build.sh153
-rw-r--r--deploy/systemd/blog-deploy.path9
-rw-r--r--deploy/systemd/blog-deploy.service6
6 files changed, 214 insertions, 0 deletions
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
new file mode 100644
index 00000000..916c8d7a
--- /dev/null
+++ b/deploy/docker-compose.yml
@@ -0,0 +1,11 @@
+services:
+ blog-nginx:
+ image: nginx:stable-alpine
+ container_name: blog-nginx
+ entrypoint: ["nginx", "-g", "daemon off;"]
+ ports:
+ - "127.0.0.1:8091:80"
+ volumes:
+ - /home/somhairle/blog-deploy/releases:/var/www/blog:ro
+ - /home/somhairle/blog-deploy/nginx-blog.conf:/etc/nginx/conf.d/default.conf:ro
+ restart: unless-stopped
diff --git a/deploy/git-hook/post-receive b/deploy/git-hook/post-receive
new file mode 100755
index 00000000..65cec98a
--- /dev/null
+++ b/deploy/git-hook/post-receive
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Runs INSIDE the gitweb container (uid 1000) on git-http-backend pushes.
+# Only writes a signal into the bare repo; the host systemd path unit
+# (/home/somhairle/git/blog.git/deploy.signal) picks it up and builds.
+set -u
+ZERO=0000000000000000000000000000000000000000
+GITDIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
+while read -r oldrev newrev refname; do
+ case "$refname" in
+ refs/heads/main)
+ if [ "$newrev" != "$ZERO" ]; then
+ if printf '%s %s\n' "$newrev" "$(date -u +%FT%TZ)" > "$GITDIR/deploy.signal" 2>/dev/null; then
+ echo "blog-deploy: build signal written for $newrev" >&2
+ else
+ echo "blog-deploy: WARNING could not write deploy.signal" >&2
+ fi
+ fi
+ ;;
+ esac
+done
+exit 0
diff --git a/deploy/nginx-blog.conf b/deploy/nginx-blog.conf
new file mode 100644
index 00000000..9c8792fd
--- /dev/null
+++ b/deploy/nginx-blog.conf
@@ -0,0 +1,14 @@
+server {
+ listen 80 default_server;
+ server_name _;
+ server_tokens off;
+
+ root /var/www/blog/current;
+ index index.html;
+ charset utf-8;
+ client_max_body_size 0;
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+}
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"
diff --git a/deploy/systemd/blog-deploy.path b/deploy/systemd/blog-deploy.path
new file mode 100644
index 00000000..0d0aa632
--- /dev/null
+++ b/deploy/systemd/blog-deploy.path
@@ -0,0 +1,9 @@
+[Unit]
+Description=Watch for deploy signals written by blog.git post-receive hook
+
+[Path]
+PathChanged=/home/somhairle/git/blog.git/deploy.signal
+Unit=blog-deploy.service
+
+[Install]
+WantedBy=default.target
diff --git a/deploy/systemd/blog-deploy.service b/deploy/systemd/blog-deploy.service
new file mode 100644
index 00000000..1d67cb80
--- /dev/null
+++ b/deploy/systemd/blog-deploy.service
@@ -0,0 +1,6 @@
+[Unit]
+Description=Blog deploy: build Hexo from blog.git main and publish atomically
+
+[Service]
+Type=oneshot
+ExecStart=/home/somhairle/blog-deploy/scripts/build.sh