5ee02c34f0
The Gitea runner is not up yet, so deploys need to be possible by hand. scripts/deploy.sh mirrors the workflow: same hugo build, same rsync invocation, same DEPLOY_PATH guards. It previews every change and reports the number of server-side deletions before asking to continue, since rsync runs with --delete. Credentials live in the gitignored scripts/deploy.env. Two Windows-specific guards, both hit in practice: it refuses an SSH key under /mnt (WSL cannot hold 0600 there, so ssh rejects it) and it fails with a clear message when run from Git Bash, which has no rsync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
4.3 KiB
Bash
Executable File
137 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Manual deploy from a workstation, for use until the Gitea runner is up.
|
|
# Mirrors .gitea/workflows/deploy.yml: same build, same rsync, same guards.
|
|
#
|
|
# ./scripts/deploy.sh build, preview the changes, confirm, deploy
|
|
# ./scripts/deploy.sh --dry-run stop after the preview
|
|
# ./scripts/deploy.sh --yes skip the confirmation prompt
|
|
# ./scripts/deploy.sh --skip-build deploy whatever is already in public/
|
|
#
|
|
# Configuration is read from scripts/deploy.env (gitignored); see
|
|
# scripts/deploy.env.example. Environment variables take precedence.
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
DRY_RUN=0
|
|
ASSUME_YES=0
|
|
SKIP_BUILD=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--dry-run) DRY_RUN=1 ;;
|
|
--yes|-y) ASSUME_YES=1 ;;
|
|
--skip-build) SKIP_BUILD=1 ;;
|
|
-h|--help) sed -n '3,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) echo "unknown option: $arg (try --help)" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
die() { echo "error: $*" >&2; exit 1; }
|
|
|
|
# ---------------------------------------------------------------- config ---
|
|
|
|
CONFIG="${DEPLOY_ENV_FILE:-scripts/deploy.env}"
|
|
if [ -f "$CONFIG" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "$CONFIG"
|
|
elif [ -z "${DEPLOY_HOST:-}" ]; then
|
|
die "no $CONFIG and no DEPLOY_HOST in the environment.
|
|
Copy scripts/deploy.env.example to $CONFIG and fill it in."
|
|
fi
|
|
|
|
: "${DEPLOY_HOST:?set DEPLOY_HOST}"
|
|
: "${DEPLOY_USER:?set DEPLOY_USER}"
|
|
: "${DEPLOY_PATH:?set DEPLOY_PATH}"
|
|
SSH_KEY="${SSH_KEY:-$HOME/.ssh/mcg-deploy}"
|
|
|
|
# rsync runs with --delete, so the destination becomes an exact mirror of
|
|
# public/. Refuse to run against an unset, root or shallow path.
|
|
case "$DEPLOY_PATH" in
|
|
""|"/"|"/root"|"/home"|"/var"|"/etc"|"/usr"|"/srv"|"/opt")
|
|
die "DEPLOY_PATH is unsafe: '$DEPLOY_PATH'" ;;
|
|
esac
|
|
if [ "$(printf '%s' "$DEPLOY_PATH" | tr -cd '/' | wc -c)" -lt 2 ]; then
|
|
die "DEPLOY_PATH '$DEPLOY_PATH' is too shallow to delete into"
|
|
fi
|
|
|
|
# --------------------------------------------------------- dependencies ---
|
|
|
|
for cmd in rsync ssh; do
|
|
command -v "$cmd" >/dev/null 2>&1 || die "'$cmd' is not installed.
|
|
Run this script from WSL or another Linux shell — Git Bash and
|
|
PowerShell do not ship rsync."
|
|
done
|
|
|
|
# ------------------------------------------------------------- ssh key ---
|
|
|
|
[ -f "$SSH_KEY" ] || die "ssh key not found: $SSH_KEY"
|
|
|
|
case "$SSH_KEY" in
|
|
/mnt/*)
|
|
die "the ssh key is on the Windows filesystem ($SSH_KEY).
|
|
WSL cannot hold 0600 there, so ssh will reject the key. Copy it into
|
|
the WSL home directory instead:
|
|
cp '$SSH_KEY' ~/.ssh/mcg-deploy && chmod 600 ~/.ssh/mcg-deploy" ;;
|
|
esac
|
|
|
|
perms="$(stat -c '%a' "$SSH_KEY" 2>/dev/null || echo '')"
|
|
if [ -n "$perms" ] && [ "$perms" != "600" ] && [ "$perms" != "400" ]; then
|
|
echo "==> Tightening permissions on $SSH_KEY (was $perms)"
|
|
chmod 600 "$SSH_KEY"
|
|
fi
|
|
|
|
# --------------------------------------------------------------- build ---
|
|
|
|
if [ "$SKIP_BUILD" -eq 0 ]; then
|
|
command -v hugo >/dev/null 2>&1 || die "hugo is not on PATH.
|
|
Either install Hugo 0.164.0 in WSL, or build on the Windows side and
|
|
re-run this script with --skip-build."
|
|
echo "==> Building"
|
|
rm -rf public
|
|
hugo --gc --minify
|
|
fi
|
|
|
|
[ -f public/index.html ] || \
|
|
die "public/index.html missing - refusing to deploy an empty build"
|
|
|
|
# -------------------------------------------------------------- deploy ---
|
|
|
|
SSH_CMD="ssh -i $SSH_KEY -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"
|
|
TARGET="${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
|
|
RSYNC_OPTS=(-az --delete --checksum)
|
|
|
|
preview="$(mktemp)"
|
|
trap 'rm -f "$preview"' EXIT
|
|
|
|
echo "==> Previewing changes against $TARGET"
|
|
rsync "${RSYNC_OPTS[@]}" --itemize-changes --dry-run \
|
|
-e "$SSH_CMD" public/ "$TARGET" | tee "$preview"
|
|
|
|
deletions="$(grep -c '^\*deleting' "$preview" || true)"
|
|
updates="$(grep -cv '^\*deleting' "$preview" || true)"
|
|
echo
|
|
echo "==> $updates file(s) to send, $deletions to delete on the server"
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
echo "==> Dry run only, nothing was uploaded."
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$ASSUME_YES" -eq 0 ]; then
|
|
printf 'Continue? [y/N] '
|
|
read -r reply
|
|
case "$reply" in
|
|
[yY]|[yY][eE][sS]) ;;
|
|
*) echo "aborted"; exit 1 ;;
|
|
esac
|
|
fi
|
|
|
|
echo "==> Deploying"
|
|
rsync "${RSYNC_OPTS[@]}" --human-readable --stats \
|
|
-e "$SSH_CMD" public/ "$TARGET"
|
|
|
|
echo "==> Done"
|