who_need_help/scripts/production-release.sh

148 lines
5.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
action=${1:-plan}
ssh_target=${2:-whoneedhelp}
remote_root=${WNH_PRODUCTION_REMOTE_ROOT:-/srv/who_need_help-production}
expected_domain=${WNH_PRODUCTION_DOMAIN:-whoneedhelp.com}
case "$action" in
plan | apply) ;;
*)
echo "Usage: $0 [plan|apply] [SSH_TARGET]" >&2
exit 2
;;
esac
for command in git pg_restore scp sha256sum ssh; do
command -v "$command" >/dev/null 2>&1 || {
echo "Required command is unavailable: $command" >&2
exit 2
}
done
local_commit=$(git -C "$ROOT" rev-parse --verify HEAD)
remote_commit=$(
ssh -o BatchMode=yes "$ssh_target" \
"git -C '$remote_root' rev-parse --verify HEAD"
)
printf 'Local candidate commit: %s\n' "$local_commit"
printf 'Current production commit: %s\n' "$remote_commit"
if git -C "$ROOT" cat-file -e "$remote_commit^{commit}" 2>/dev/null; then
git -C "$ROOT" merge-base --is-ancestor "$remote_commit" "$local_commit" || {
echo "The local candidate is not a fast-forward from the production commit." >&2
exit 2
}
printf 'Pending commits: %s\n' \
"$(git -C "$ROOT" rev-list --count "$remote_commit..$local_commit")"
else
echo "The production commit is not present in the local object database." >&2
exit 2
fi
migration_policy_output=$(
"$ROOT/scripts/release-migration-policy.sh" "$remote_commit" "$local_commit"
)
printf '%s\n' "$migration_policy_output"
migration_policy=$(
awk -F= '$1 == "migration_policy" {print $2}' <<<"$migration_policy_output"
)
plan_failed=0
if ! ssh -o BatchMode=yes "$ssh_target" \
"bash -s -- plan '$remote_root' '$expected_domain'" \
<"$ROOT/scripts/production-release-remote.sh"; then
plan_failed=1
fi
if ! ssh -o BatchMode=yes "$ssh_target" \
"bash -s -- '$remote_root/.env' --check-only production" \
<"$ROOT/scripts/backup-external-postgres.sh"; then
plan_failed=1
fi
if ! ssh -o BatchMode=yes "$ssh_target" \
"bash -s -- '$remote_root/.env' --require-release" \
<"$ROOT/scripts/check-environment-readiness.sh"; then
plan_failed=1
fi
if [[ "$plan_failed" -ne 0 ]]; then
echo "Production release plan has blocking checks; no remote state was changed." >&2
exit 1
fi
if [[ "$action" == "plan" ]]; then
echo "Production release plan passed; no remote state was changed."
exit 0
fi
if [[ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no)" ]]; then
echo "Refusing to release a dirty tracked checkout." >&2
exit 2
fi
confirmation="$expected_domain:$local_commit"
if [[ "${WNH_PRODUCTION_RELEASE_CONFIRM:-}" != "$confirmation" ]]; then
echo "Release execution requires explicit approval in this exact process:" >&2
echo "WNH_PRODUCTION_RELEASE_CONFIRM=$confirmation $0 apply $ssh_target" >&2
exit 2
fi
if [[ "$migration_policy" == "forward_only" ]]; then
forward_confirmation="$expected_domain:$local_commit:forward-only"
if [[ "${WNH_PRODUCTION_FORWARD_ONLY_CONFIRM:-}" != "$forward_confirmation" ]]; then
echo "This release contains migrations that are not safe for an automatic old-image rollback." >&2
echo "A failed deployment after migration starts will keep the old application stopped." >&2
echo "Review the migration and recovery plan, then approve this exact boundary:" >&2
echo "WNH_PRODUCTION_FORWARD_ONLY_CONFIRM=$forward_confirmation \\" >&2
echo " WNH_PRODUCTION_RELEASE_CONFIRM=$confirmation $0 apply $ssh_target" >&2
exit 2
fi
fi
"$ROOT/scripts/prepare-production-release.sh"
release_dir="$ROOT/output/releases/$local_commit"
bundle="$release_dir/who_need_help-$local_commit.bundle"
remote_release_dir="$remote_root/output/releases/incoming"
remote_bundle="$remote_release_dir/$(basename -- "$bundle")"
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
remote_backup="$remote_root/output/backups/production/pre-$timestamp-${local_commit:0:12}.dump"
ssh -o BatchMode=yes "$ssh_target" \
"install -d -m 700 '$remote_release_dir'"
scp -p "$bundle" "$bundle.sha256" "$ssh_target:$remote_release_dir/"
ssh -o BatchMode=yes "$ssh_target" \
"bash -s -- '$remote_root/.env' '$remote_backup' production" \
<"$ROOT/scripts/backup-external-postgres.sh"
local_backup_dir="$ROOT/output/production-backups/$timestamp-${local_commit:0:12}"
mkdir -p "$local_backup_dir"
chmod 700 "$ROOT/output" "$ROOT/output/production-backups" "$local_backup_dir"
scp -p \
"$ssh_target:$remote_backup" \
"$ssh_target:$remote_backup.sha256" \
"$ssh_target:$remote_backup.metadata" \
"$local_backup_dir/"
(
cd "$local_backup_dir"
sha256sum --check "$(basename -- "$remote_backup.sha256")" >/dev/null
)
pg_restore --list "$local_backup_dir/$(basename -- "$remote_backup")" >/dev/null
echo "Copied and independently verified the pre-release backup outside the production server."
quoted_confirmation=$(printf '%q' "$confirmation")
quoted_forward_confirmation=$(
printf '%q' "${WNH_PRODUCTION_FORWARD_ONLY_CONFIRM:-}"
)
ssh -o BatchMode=yes "$ssh_target" \
"WNH_PRODUCTION_RELEASE_CONFIRM=$quoted_confirmation WNH_PRODUCTION_FORWARD_ONLY_CONFIRM=$quoted_forward_confirmation bash -s -- apply '$remote_root' '$expected_domain' '$remote_bundle' '$local_commit' '$remote_backup' '$migration_policy'" \
<"$ROOT/scripts/production-release-remote.sh"
echo "Production release and public health verification completed."