Files
probably-rendering-one-more…/scripts/deploy.sh
T
codex-bot 7f88ebba71
Unified application CI and deployment / static-checks (push) Successful in 1m9s
python CI and deployment / static-checks (push) Successful in 1m7s
Unified application CI and deployment / static-checks (pull_request) Successful in 1m7s
python CI and deployment / static-checks (pull_request) Successful in 1m6s
Unified application CI and deployment / verify-and-release (push) Has been skipped
python CI and deployment / verify-and-deploy (push) Has been skipped
Unified application CI and deployment / verify-and-release (pull_request) Has been skipped
python CI and deployment / verify-and-deploy (pull_request) Has been skipped
Run only static checks before main and gate full release on main push
2026-11-04 04:24:31 +08:00

159 lines
8.6 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
if [ "$(id -u)" -ne 0 ]; then exec sudo -n bash "$0" "$@"; fi
RELEASE_SHA="${1:?Release SHA is required}"
[[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo 'Invalid release SHA'; exit 1; }
IMAGE="waterfall-wiki:$RELEASE_SHA"
APP_ROOT=/opt/prompt
RELEASE_DIR="$APP_ROOT/releases/$RELEASE_SHA"
PORT=18131
command -v docker >/dev/null
docker compose version >/dev/null
docker image inspect "$IMAGE" >/dev/null
mkdir -p "$APP_ROOT/releases"
exec 9>"$APP_ROOT/.deploy.lock"
flock -w 300 9 || { echo 'Another PROMPT deployment is running'; exit 1; }
test "$(git -c safe.directory="$PWD" rev-parse HEAD)" = "$RELEASE_SHA"
LATEST_MAIN=$(git -c safe.directory="$PWD" ls-remote origin refs/heads/main | cut -f1)
if [ "$LATEST_MAIN" != "$RELEASE_SHA" ]; then
git -c safe.directory="$PWD" fetch origin main
if ! git -c safe.directory="$PWD" diff --quiet "$RELEASE_SHA" origin/main -- apps/waterfall-wiki packages/brand Dockerfile .dockerignore compose.yml package.json pnpm-workspace.yaml scripts .gitea/workflows/app.yml; then
echo 'Skipping deployment: Wiki has a newer commit'; exit 0
fi
fi
PREVIOUS=""
PREVIOUS_WEB=web
PREVIOUS_WORKER=worker
if [ -L "$APP_ROOT/current" ] && [ -d "$APP_ROOT/current" ]; then PREVIOUS=$(readlink -f "$APP_ROOT/current"); fi
if [ -n "$PREVIOUS" ] && grep -q '^ waterfall-wiki:' "$PREVIOUS/compose.yml"; then
PREVIOUS_WEB=waterfall-wiki; PREVIOUS_WORKER=waterfall-wiki-worker
fi
bash scripts/infra.sh "$IMAGE"
mkdir -p "$RELEASE_DIR"
install -m 0644 compose.yml "$RELEASE_DIR/compose.yml"
printf 'WATERFALL_WIKI_IMAGE=%s\nRELEASE_SHA=%s\nWEB_PORT=%s\nAUTH_CONFIG_FILE=/auth/config.json\n' "$IMAGE" "$RELEASE_SHA" "$PORT" > "$RELEASE_DIR/release.env"
compose_at(){ local release="$1"; shift; env -u PROMPT_IMAGE -u PROMPT_TEST_IMAGE -u AUTH_CONFIG_FILE -u AUTH_COOKIE_SECURE -u WATERFALL_WIKI_IMAGE -u WATERFALL_WIKI_TEST_IMAGE -u RELEASE_SHA -u WEB_PORT -u WEB_BIND_ADDRESS docker compose -p prompt --env-file /etc/prompt.env --env-file "$release/release.env" -f "$release/compose.yml" "$@"; }
compose(){ compose_at "$RELEASE_DIR" "$@"; }
LEGACY_WEB=0
LEGACY_WORKER=0
LEGACY_WEB_ENABLED=0
LEGACY_WORKER_ENABLED=0
if command -v systemctl >/dev/null; then
if systemctl is-active --quiet prompt.service; then LEGACY_WEB=1; fi
if systemctl is-active --quiet prompt-worker.service; then LEGACY_WORKER=1; fi
if systemctl is-enabled --quiet prompt.service 2>/dev/null; then LEGACY_WEB_ENABLED=1; fi
if systemctl is-enabled --quiet prompt-worker.service 2>/dev/null; then LEGACY_WORKER_ENABLED=1; fi
fi
CUTOVER=0
MIGRATION_STARTED=0
BACKUP_FILE=""
rollback(){
local code=$?
trap - ERR
set +e
if [ "$CUTOVER" -eq 1 ]; then
compose stop waterfall-wiki waterfall-wiki-worker
if [ "$MIGRATION_STARTED" -eq 1 ] && [ -s "$BACKUP_FILE" ]; then
if ! compose exec -T postgres pg_restore -U prompt -d prompt --clean --if-exists --exit-on-error < "$BACKUP_FILE"; then
echo 'Database rollback failed; application remains stopped. Backup retained.'
exit "$code"
fi
echo 'Restored the database backup compatible with the previous application'
fi
if [ -n "$PREVIOUS" ] && [ -f "$PREVIOUS/compose.yml" ]; then
compose_at "$PREVIOUS" up -d --no-build --no-deps "$PREVIOUS_WEB" "$PREVIOUS_WORKER"
echo 'Restored the previous Docker application image'
else
compose stop waterfall-wiki waterfall-wiki-worker
compose rm -f waterfall-wiki waterfall-wiki-worker
if [ "$LEGACY_WORKER_ENABLED" -eq 1 ]; then systemctl enable prompt-worker.service; fi
if [ "$LEGACY_WEB_ENABLED" -eq 1 ]; then systemctl enable prompt.service; fi
if [ "$LEGACY_WEB" -eq 1 ]; then systemctl start prompt.service; fi
if [ "$LEGACY_WORKER" -eq 1 ]; then systemctl start prompt-worker.service; fi
echo 'Restored legacy application services if previously running'
fi
if [ -n "$PREVIOUS" ]; then
ln -sfn "$PREVIOUS" "$APP_ROOT/.current.rollback"
mv -Tf "$APP_ROOT/.current.rollback" "$APP_ROOT/current"
fi
fi
echo "Docker deployment failed (exit $code); persistent data volumes were retained"
exit "$code"
}
trap rollback ERR
# Keep the same prompt_postgres_data and prompt_objects volumes. No production down -v.
# Infrastructure already exists; never recreate it during an app release.
compose exec -T postgres pg_isready -U prompt -d prompt
install -d -o root -g root -m 0700 /var/lib/prompt/backups
CUTOVER=1
compose stop waterfall-wiki waterfall-wiki-worker
if [ -n "$PREVIOUS" ]; then compose_at "$PREVIOUS" stop "$PREVIOUS_WEB" "$PREVIOUS_WORKER"; fi
BACKUP_FILE="/var/lib/prompt/backups/$(date -u +%Y%m%dT%H%M%S)-$RELEASE_SHA.dump"
compose exec -T postgres pg_dump -U prompt -d prompt -Fc > "$BACKUP_FILE"
find /var/lib/prompt/backups -name '*.dump' -printf '%T@ %p\n' | sort -rn | tail -n +11 | cut -d' ' -f2- | xargs -r rm --
MIGRATION_STARTED=1
compose run --rm --no-deps auth-admin
compose run --rm --no-deps migrate
compose run --rm --no-deps publish
CUTOVER=1
# One-time migration: finish a current host worker job before switching the web port.
if [ "$LEGACY_WORKER" -eq 1 ] || [ "$LEGACY_WORKER_ENABLED" -eq 1 ]; then systemctl disable --now prompt-worker.service; fi
if [ "$LEGACY_WEB" -eq 1 ] || [ "$LEGACY_WEB_ENABLED" -eq 1 ]; then systemctl disable --now prompt.service; fi
compose up -d --no-build --no-deps --wait --wait-timeout 180 waterfall-wiki waterfall-wiki-worker
compose exec -T waterfall-wiki node container-health.mjs
compose exec -T waterfall-wiki node auth-cli.cjs verify
compose exec -T waterfall-wiki node auth-cli.cjs run scripts/import-character-packs.mjs
compose exec -T waterfall-wiki-worker sh -c 'test "$(id -u)" -ne 0 && ffmpeg -version >/dev/null'
# Editorial posts may be older than the first 24 entries. Verify the complete paged feed.
compose exec -T waterfall-wiki sh -c 'cat > /tmp/prompt-release-check.mjs && node auth-cli.cjs run /tmp/prompt-release-check.mjs' <<'NODE'
(async()=>{
const slugs=['learning-skills-notes','learning-skills-teaching','learning-skills-english'];
const missing=new Set(slugs),seen=new Set();let cursor='';
while(missing.size){
const query=new URLSearchParams({section:'articles',...(cursor?{cursor}:{})});
const response=await fetch('http://127.0.0.1:3000/api/feed?'+query);
if(!response.ok)throw Error('Article feed unavailable: '+response.status);
const feed=await response.json();for(const post of feed.posts)missing.delete(post.slug);
if(!missing.size||!feed.nextCursor)break;
if(seen.has(feed.nextCursor))throw Error('Article pagination did not advance');
seen.add(feed.nextCursor);cursor=feed.nextCursor;
}
if(missing.size)throw Error('Published articles missing: '+[...missing].join(', '));
for(const slug of slugs){const res=await fetch('http://127.0.0.1:3000/p/'+slug);if(!res.ok)throw Error('Article unavailable: '+slug);}
console.log('Verified three published article pages across the paged feed');
})().catch(error=>{console.error(error.message);process.exit(1)});
NODE
bash scripts/verify-studio-production.sh
ln -sfn "$RELEASE_DIR" "$APP_ROOT/.current.next"
mv -Tf "$APP_ROOT/.current.next" "$APP_ROOT/current"
install -m 0755 scripts/auth-admin.sh "$APP_ROOT/auth"
compose ps
for service in waterfall-wiki waterfall-wiki-worker postgres objects; do
id=$(compose ps -q "$service")
docker inspect --format '{{.Name}} running={{.State.Running}} restart={{.HostConfig.RestartPolicy.Name}}' "$id"
done
if command -v systemctl >/dev/null; then
if systemctl is-active --quiet prompt.service || systemctl is-active --quiet prompt-worker.service; then echo 'Legacy host service is unexpectedly running'; false; fi
fi
if docker inspect prompt-studio-studio-1 >/dev/null 2>&1; then
if [ "$(docker inspect --format '{{index .Config.Labels "com.docker.compose.project"}}' prompt-studio-studio-1)" = prompt-studio ]; then
docker stop prompt-studio-studio-1
fi
fi
echo "Full Docker deployment verified: web+worker+postgres+objects port=$PORT release=$RELEASE_SHA"
# Register only after every deployment check succeeded; ledger failure retains old images.
if ! python3 scripts/wiki-image-lifecycle.py record-success --sha "$RELEASE_SHA"; then
echo "Warning: successful-release ledger update failed; runtime cleanup stays conservative" >&2
fi
# Read-only network diagnostics: a private address alone is not a public entrypoint.
echo "Host: $(hostname)"
hostname -I || true
if command -v curl >/dev/null; then
for field in eipv4 public-ipv4; do
address=$(curl --noproxy '*' -fsS --connect-timeout 2 --max-time 4 "http://100.100.100.200/latest/meta-data/$field" 2>/dev/null || true)
if [[ "$address" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Instance $field: $address"; fi
done
fi