#!/usr/bin/env bash set -euo pipefail umask 077 ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) BASE_IMAGE="debian:trixie-slim@sha256:020c0d20b9880058cbe785a9db107156c3c75c2ac944a6aa7ab59f2add76a7bd" mkdir -p "$ROOT/output" run_dir=$(mktemp -d "$ROOT/output/test-release-drill.XXXXXX") fixture="$run_dir/test" mock_bin="$run_dir/mock-bin" remote_root=/srv/who_need_help-test current_commit=1111111111111111111111111111111111111111 target_commit=2222222222222222222222222222222222222222 release_confirmation="test.whoneedhelp.com:$target_commit" forward_confirmation="test.whoneedhelp.com:$target_commit:forward-only" cleanup() { status=$? trap - EXIT HUP INT TERM if [[ "$status" -ne 0 ]]; then for output in "$run_dir"/*.out; do [[ -f "$output" ]] || continue printf '\n--- %s ---\n' "$(basename -- "$output")" >&2 sed -n '1,240p' "$output" >&2 done fi find "$run_dir" -xdev -depth -delete 2>/dev/null || true exit "$status" } trap cleanup EXIT HUP INT TERM install -d -m 700 \ "$fixture/.git" \ "$fixture/scripts" \ "$fixture/output/releases/incoming" \ "$fixture/output/backups/test" \ "$mock_bin" write_old_env() { install -m 600 /dev/null "$fixture/.env" printf '%s\n' \ 'DEPLOYMENT_ENV=test' \ 'COMPOSE_PROJECT_NAME=who_need_help_test' \ 'DATABASE_MODE=container' \ 'APP_TOPOLOGY=compact' \ 'PHX_HOST=test.whoneedhelp.com' \ 'WNH_BASE_URL=https://test.whoneedhelp.com' \ "APP_IMAGE=who-need-help:test-${current_commit:0:12}" \ "SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-test-${current_commit:0:12}" \ "POSTGIS_IMAGE=who-need-help:postgis-test-${current_commit:0:12}" \ 'POSTGRES_DB=who_need_help_test' \ 'POSTGRES_USER=who_need_help_test' \ 'POSTGRES_PASSWORD=test-release-drill-password' \ 'DATABASE_URL=ecto://who_need_help_test:test-release-drill-password@db/who_need_help_test' \ >"$fixture/.env" } write_old_env bundle="$fixture/output/releases/incoming/who_need_help-$target_commit.bundle" printf 'isolated test release drill bundle\n' >"$bundle" bundle_hash=$(sha256sum "$bundle" | awk '{print $1}') printf '%s %s\n' "$bundle_hash" "$(basename -- "$bundle")" >"$bundle.sha256" image_archive="$fixture/output/releases/incoming/who_need_help-$target_commit-test-images-linux-amd64.tar.gz" printf 'isolated test release drill image archive\n' | gzip -n >"$image_archive" image_hash=$(sha256sum "$image_archive" | awk '{print $1}') printf '%s %s\n' "$image_hash" "$(basename -- "$image_archive")" \ >"$image_archive.sha256" app_config="$run_dir/app-image-config.json" db_config="$run_dir/db-image-config.json" printf '%s\n' \ '{"architecture":"amd64","os":"linux","rootfs":{"type":"layers","diff_ids":[]}}' \ >"$app_config" printf '%s\n' \ '{"architecture":"amd64","os":"linux","variant":"test-db","rootfs":{"type":"layers","diff_ids":[]}}' \ >"$db_config" app_image_id="sha256:$(sha256sum "$app_config" | awk '{print $1}')" db_image_id="sha256:$(sha256sum "$db_config" | awk '{print $1}')" [[ "$app_image_id" =~ ^sha256:[0-9a-f]{64}$ ]] [[ "$db_image_id" =~ ^sha256:[0-9a-f]{64}$ ]] image_manifest="$fixture/output/releases/incoming/who_need_help-$target_commit-test-images.manifest" printf '%s\n' \ 'format=1' \ 'deployment=test' \ "commit=$target_commit" \ 'platform=linux/amd64' \ 'topology=compact' \ 'image_count=2' \ "image=who-need-help:test-${target_commit:0:12}|$app_image_id" \ "image=who-need-help:postgis-test-${target_commit:0:12}|$db_image_id" \ >"$image_manifest" backup="$fixture/output/backups/test/pre-release.dump" printf 'isolated test release drill backup\n' >"$backup" backup_hash=$(sha256sum "$backup" | awk '{print $1}') printf '%s %s\n' "$backup_hash" "$(basename -- "$backup")" >"$backup.sha256" chmod 600 "$bundle" "$bundle.sha256" "$image_archive" \ "$image_archive.sha256" "$image_manifest" "$backup" "$backup.sha256" for script in validate-test-env.sh verify-realtime-cluster.sh \ verify-beam-runtime.sh check-database.sh; do install -m 755 /dev/null "$fixture/scripts/$script" printf '%s\n' '#!/bin/sh' 'exit 0' >"$fixture/scripts/$script" done install -m 755 /dev/null "$fixture/scripts/restore-drill-compose.sh" printf '%s\n' \ '#!/bin/sh' \ 'set -eu' \ "printf 'restore-drill:%s\\n' \"\$1\" >>\"\$MOCK_COMMAND_LOG\"" \ >"$fixture/scripts/restore-drill-compose.sh" install -m 755 /dev/null "$fixture/scripts/set-deployment-revision.sh" cat >"$fixture/scripts/set-deployment-revision.sh" <<'EOF' #!/bin/sh set -eu env_file=$1 short=${MOCK_TARGET_COMMIT%${MOCK_TARGET_COMMIT#????????????}} sed -i \ -e "s|^APP_IMAGE=.*|APP_IMAGE=who-need-help:test-$short|" \ -e "s|^SOCKET_PROXY_IMAGE=.*|SOCKET_PROXY_IMAGE=who-need-help:socket-proxy-test-$short|" \ -e "s|^POSTGIS_IMAGE=.*|POSTGIS_IMAGE=who-need-help:postgis-test-$short|" \ "$env_file" EOF install -m 755 /dev/null "$fixture/scripts/compose.sh" cat >"$fixture/scripts/compose.sh" <<'EOF' #!/bin/sh set -eu env_file=$1 shift case "$*" in 'config --quiet') exit 0 ;; 'ps -q db') printf 'db-1\n'; exit 0 ;; 'ps -q app') printf 'app-1\n'; exit 0 ;; 'stop app') printf 'compose:%s\n' "$*" >>"$MOCK_COMMAND_LOG" exit 0 ;; 'up -d --no-build --wait db') printf 'compose:%s\n' "$*" >>"$MOCK_COMMAND_LOG" exit 0 ;; 'run --rm --no-deps --interactive=false migrate') printf 'compose:%s\n' "$*" >>"$MOCK_COMMAND_LOG" exit 0 ;; 'up -d --no-deps --no-build --force-recreate --wait app') printf 'compose:%s\n' "$*" >>"$MOCK_COMMAND_LOG" if [ "${MOCK_FAIL_APP_UP:-}" = once ] && [ ! -e "$MOCK_FAIL_APP_MARKER" ]; then : >"$MOCK_FAIL_APP_MARKER" exit 23 fi if [ "${MOCK_FAIL_RECOVERY:-}" = true ] && grep -q "^APP_IMAGE=who-need-help:test-${MOCK_CURRENT_COMMIT%${MOCK_CURRENT_COMMIT#????????????}}$" "$env_file"; then exit 24 fi exit 0 ;; esac printf 'Unexpected compose invocation: %s\n' "$*" >&2 exit 1 EOF printf '%s\n' "$current_commit" >"$fixture/git-state" install -m 755 /dev/null "$mock_bin/git" cat >"$mock_bin/git" <<'EOF' #!/bin/sh set -eu case " $* " in *' status --porcelain --untracked-files=normal '*) exit 0 ;; *' rev-parse --verify HEAD '*) cat "$MOCK_GIT_STATE"; exit 0 ;; *' rev-parse refs/wnh/test-releases/'*'^{commit} '*) printf '%s\n' "$MOCK_TARGET_COMMIT"; exit 0 ;; *' bundle verify '*) exit 0 ;; *' bundle list-heads '*) printf '%s HEAD\n' "$MOCK_TARGET_COMMIT"; exit 0 ;; *' fetch '*) printf 'git:%s\n' "$*" >>"$MOCK_COMMAND_LOG"; exit 0 ;; *' merge-base --is-ancestor '*) exit 0 ;; *' show refs/wnh/test-releases/'*':scripts/release-migration-policy.sh '*) cat <<'POLICY' #!/bin/sh set -eu printf 'migration_policy=%s\n' "$MOCK_MIGRATION_POLICY" printf 'migration_versions=20260101000000:%s\n' "$MOCK_MIGRATION_POLICY" printf 'migration_count=1\n' POLICY exit 0 ;; *' checkout --detach refs/wnh/test-releases/'*) printf '%s\n' "$MOCK_TARGET_COMMIT" >"$MOCK_GIT_STATE" exit 0 ;; *" checkout --detach $MOCK_CURRENT_COMMIT "*) printf '%s\n' "$MOCK_CURRENT_COMMIT" >"$MOCK_GIT_STATE" exit 0 ;; esac printf 'Unexpected git invocation: %s\n' "$*" >&2 exit 1 EOF install -m 755 /dev/null "$mock_bin/docker" cat >"$mock_bin/docker" <<'EOF' #!/bin/sh set -eu if [ "$1" = inspect ]; then case "$3" in '{{.State.Status}}') printf 'running\n' ;; '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}') printf 'healthy\n' ;; '{{.Config.Image}}') if [ "${MOCK_WRONG_RUNTIME_IMAGE:-}" = true ]; then printf 'who-need-help:test-wrong\n' elif [ "$4" = db-1 ]; then awk -F= '$1 == "POSTGIS_IMAGE" {print substr($0, index($0, "=") + 1)}' \ "$MOCK_ENV_FILE" else awk -F= '$1 == "APP_IMAGE" {print substr($0, index($0, "=") + 1)}' \ "$MOCK_ENV_FILE" fi ;; '{{.Image}}') if [ "$4" = db-1 ]; then printf '%s\n' "$DB_IMAGE_ID" else printf '%s\n' "$APP_IMAGE_ID" fi ;; *) exit 1 ;; esac exit 0 fi if [ "$1" = image ] && [ "$2" = inspect ] && [ "$3" = --format ]; then image=$5 case "$4" in '{{.Id}}') case "$image" in who-need-help:postgis-test-*) printf '%s\n' "$DB_IMAGE_ID" ;; *) printf '%s\n' "$APP_IMAGE_ID" ;; esac ;; '{{.Os}}/{{.Architecture}}') printf 'linux/amd64\n' ;; *) exit 1 ;; esac exit 0 fi if [ "$1" = load ]; then cat >/dev/null printf 'docker:load\n' >>"$MOCK_COMMAND_LOG" exit 0 fi printf 'Unexpected docker invocation: %s\n' "$*" >&2 exit 1 EOF for command in curl jq pg_restore; do install -m 755 /dev/null "$mock_bin/$command" printf '%s\n' '#!/bin/sh' 'exit 0' >"$mock_bin/$command" done install -m 755 /dev/null "$mock_bin/flock" cat >"$mock_bin/flock" <<'EOF' #!/bin/sh set -eu if [ "${MOCK_LOCK_BUSY:-}" = true ]; then exit 1 fi exec /usr/bin/flock "$@" EOF touch "$fixture/mock-commands.log" chmod 600 "$fixture/mock-commands.log" "$fixture/git-state" container_env=( --env "MOCK_TARGET_COMMIT=$target_commit" --env "MOCK_CURRENT_COMMIT=$current_commit" --env "MOCK_GIT_STATE=$remote_root/git-state" --env "MOCK_COMMAND_LOG=$remote_root/mock-commands.log" --env "MOCK_ENV_FILE=$remote_root/.env" --env "APP_IMAGE_ID=$app_image_id" --env "DB_IMAGE_ID=$db_image_id" --env "PATH=/mock-bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ) container_mounts=( --volume "$fixture:$remote_root" --volume "$mock_bin:/mock-bin:ro" --volume "$ROOT/scripts/test-release-remote.sh:/runner/test-release-remote.sh:ro" ) run_release() { local policy=$1 shift docker run --rm \ --network none \ --user "$(id -u):$(id -g)" \ --read-only \ --tmpfs /tmp:rw,nosuid,nodev,noexec \ --cap-drop ALL \ --security-opt no-new-privileges \ --env "MOCK_MIGRATION_POLICY=$policy" \ --env "WNH_TEST_RELEASE_CONFIRM=$release_confirmation" \ --env "WNH_TEST_FORWARD_ONLY_CONFIRM=$forward_confirmation" \ "$@" \ "${container_env[@]}" \ "${container_mounts[@]}" \ "$BASE_IMAGE" \ bash -c 'bash -s -- "$@" < /runner/test-release-remote.sh' _ \ apply "$remote_root" test.whoneedhelp.com \ "$remote_root/output/releases/incoming/$(basename -- "$bundle")" \ "$target_commit" \ "$remote_root/output/backups/test/$(basename -- "$backup")" \ "$policy" \ "$remote_root/output/releases/incoming/$(basename -- "$image_archive")" \ "$remote_root/output/releases/incoming/$(basename -- "$image_manifest")" } run_release forward_only >"$run_dir/forward-success.out" grep -Fx "APP_IMAGE=who-need-help:test-${target_commit:0:12}" "$fixture/.env" >/dev/null grep -Fx "POSTGIS_IMAGE=who-need-help:postgis-test-${target_commit:0:12}" "$fixture/.env" >/dev/null grep -Fx 'docker:load' "$fixture/mock-commands.log" >/dev/null grep -F 'restore-drill:' "$fixture/mock-commands.log" >/dev/null grep -F 'compose:stop app' "$fixture/mock-commands.log" >/dev/null grep -F 'compose:run --rm --no-deps --interactive=false migrate' \ "$fixture/mock-commands.log" >/dev/null grep -F 'compose:up -d --no-deps --no-build --force-recreate --wait app' \ "$fixture/mock-commands.log" >/dev/null if grep -F 'compose:build' "$fixture/mock-commands.log" >/dev/null; then echo "Test release drill unexpectedly built images on the target host." >&2 exit 1 fi grep -R -F 'status=success' \ "$fixture/output/releases" --include rollback-manifest.txt >/dev/null verified_manifest="$fixture/output/releases/verified/$target_commit.manifest" test "$(stat -c '%a' "$verified_manifest")" = 600 grep -Fx 'format=1' "$verified_manifest" >/dev/null grep -Fx 'deployment=test' "$verified_manifest" >/dev/null grep -Fx "commit=$target_commit" "$verified_manifest" >/dev/null grep -Fx 'domain=test.whoneedhelp.com' "$verified_manifest" >/dev/null grep -Fx 'status=success' "$verified_manifest" >/dev/null grep -Fx 'public_health=passed' "$verified_manifest" >/dev/null rm -f "$verified_manifest" write_old_env printf '%s\n' "$current_commit" >"$fixture/git-state" : >"$fixture/mock-commands.log" rm -f "$fixture/app-up-failed" set +e run_release forward_only \ --env MOCK_FAIL_APP_UP=once \ --env "MOCK_FAIL_APP_MARKER=$remote_root/app-up-failed" \ >"$run_dir/forward-failure.out" 2>&1 forward_status=$? set -e [[ "$forward_status" -ne 0 ]] || { echo "Forward-only test drill did not surface the startup failure." >&2 exit 1 } grep -F 'automatic old-image restart is blocked' "$run_dir/forward-failure.out" >/dev/null test ! -e "$verified_manifest" grep -Fx "APP_IMAGE=who-need-help:test-${target_commit:0:12}" "$fixture/.env" >/dev/null test "$(grep -Fc 'compose:up -d --no-deps --no-build --force-recreate --wait app' \ "$fixture/mock-commands.log")" = 1 write_old_env printf '%s\n' "$current_commit" >"$fixture/git-state" : >"$fixture/mock-commands.log" rm -f "$fixture/app-up-failed" set +e run_release application_safe \ --env MOCK_FAIL_APP_UP=once \ --env "MOCK_FAIL_APP_MARKER=$remote_root/app-up-failed" \ >"$run_dir/safe-failure.out" 2>&1 safe_status=$? set -e [[ "$safe_status" -ne 0 ]] || { echo "Application-safe test drill did not surface the startup failure." >&2 exit 1 } grep -F 'restoring the previous revision' "$run_dir/safe-failure.out" >/dev/null grep -Fx "APP_IMAGE=who-need-help:test-${current_commit:0:12}" "$fixture/.env" >/dev/null grep -Fx "$current_commit" "$fixture/git-state" >/dev/null test "$(grep -Fc 'compose:up -d --no-deps --no-build --force-recreate --wait app' \ "$fixture/mock-commands.log")" = 2 grep -R -F 'status=runtime-rolled-back' \ "$fixture/output/releases" --include rollback-manifest.txt >/dev/null write_old_env printf '%s\n' "$current_commit" >"$fixture/git-state" : >"$fixture/mock-commands.log" rm -f "$fixture/app-up-failed" set +e run_release application_safe \ --env MOCK_FAIL_APP_UP=once \ --env MOCK_FAIL_RECOVERY=true \ --env "MOCK_FAIL_APP_MARKER=$remote_root/app-up-failed" \ >"$run_dir/rollback-failure.out" 2>&1 rollback_status=$? set -e [[ "$rollback_status" -ne 0 ]] || { echo "Test drill did not surface a failed automatic rollback." >&2 exit 1 } grep -F 'Automatic test rollback failed' "$run_dir/rollback-failure.out" >/dev/null grep -R -F 'status=rollback-failed' \ "$fixture/output/releases" --include rollback-manifest.txt >/dev/null write_old_env printf '%s\n' "$current_commit" >"$fixture/git-state" : >"$fixture/mock-commands.log" set +e run_release application_safe --env MOCK_LOCK_BUSY=true \ >"$run_dir/lock-busy.out" 2>&1 lock_status=$? set -e [[ "$lock_status" -ne 0 ]] || { echo "Test drill did not reject a concurrent release lock." >&2 exit 1 } grep -F 'Another test release already holds' "$run_dir/lock-busy.out" >/dev/null test ! -s "$fixture/mock-commands.log" write_old_env printf 'DEPLOYMENT_ENV=test\n' >>"$fixture/.env" : >"$fixture/mock-commands.log" set +e run_release application_safe >"$run_dir/duplicate-env.out" 2>&1 duplicate_status=$? set -e [[ "$duplicate_status" -ne 0 ]] || { echo "Test drill accepted a duplicate critical environment key." >&2 exit 1 } grep -F 'Expected exactly one DEPLOYMENT_ENV entry' \ "$run_dir/duplicate-env.out" >/dev/null test ! -s "$fixture/mock-commands.log" echo "Isolated test release success/failure/rollback/lock/env drill passed."