#!/usr/bin/env bash set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) validator=$root/scripts/ci-validate-migrations.mjs fixture_root=$(mktemp -d) trap 'rm -rf "$fixture_root"' EXIT new_repo() { local name=$1 local repo=$fixture_root/$name mkdir -p "$repo/apps/api/migrations" git -C "$repo" init -q git -C "$repo" config user.name 'Release Migration Test' git -C "$repo" config user.email 'release-migration-test@example.invalid' printf 'CREATE TABLE accounts (id bigint PRIMARY KEY);\n' > \ "$repo/apps/api/migrations/0001_init.sql" git -C "$repo" add apps/api/migrations/0001_init.sql git -C "$repo" commit -qm baseline printf '%s\n' "$repo" } commit_all() { local repo=$1 local message=$2 git -C "$repo" add -A git -C "$repo" commit -qm "$message" } expect_pass() { local repo=$1 local base=$2 (cd "$repo" && node "$validator" "$base") } expect_fail() { local repo=$1 local base=$2 local expected=$3 local output status set +e output=$(cd "$repo" && node "$validator" "$base" 2>&1) status=$? set -e if [[ $status -eq 0 ]]; then printf 'migration validator unexpectedly passed: %s\n' "$expected" >&2 exit 1 fi grep -Fq "$expected" <<<"$output" || { printf 'missing expected migration failure %q in:\n%s\n' "$expected" "$output" >&2 exit 1 } } repo=$(new_repo safe) base=$(git -C "$repo" rev-parse HEAD) cat >"$repo/apps/api/migrations/0002_expand_accounts.sql" <<'SQL' -- DROP in a comment and string is not executable. ALTER TABLE accounts ADD COLUMN display_name text; CREATE TABLE audit_events (id bigint PRIMARY KEY, note text); INSERT INTO audit_events (id, note) VALUES (1, 'DROP TABLE is documentation'); UPDATE accounts SET display_name = 'ready' WHERE display_name IS NULL; SQL commit_all "$repo" safe expect_pass "$repo" "$base" repo=$(new_repo immutable) base=$(git -C "$repo" rev-parse HEAD) printf '\n-- retroactive edit\n' >>"$repo/apps/api/migrations/0001_init.sql" commit_all "$repo" immutable expect_fail "$repo" "$base" 'applied migrations are immutable (status M)' repo=$(new_repo removed) base=$(git -C "$repo" rev-parse HEAD) git -C "$repo" rm -q apps/api/migrations/0001_init.sql git -C "$repo" commit -qm removed expect_fail "$repo" "$base" 'applied migrations are immutable (status D)' repo=$(new_repo destructive) base=$(git -C "$repo" rev-parse HEAD) printf 'DROP TABLE accounts;\n' >"$repo/apps/api/migrations/0002_drop.sql" commit_all "$repo" destructive expect_fail "$repo" "$base" 'destructive DROP operation' repo=$(new_repo incompatible) base=$(git -C "$repo" rev-parse HEAD) printf 'ALTER TABLE accounts ADD COLUMN external_id text NOT NULL;\n' > \ "$repo/apps/api/migrations/0002_incompatible.sql" commit_all "$repo" incompatible expect_fail "$repo" "$base" 'non-null column addition' repo=$(new_repo transaction) base=$(git -C "$repo" rev-parse HEAD) printf 'ALTER TABLE accounts ADD COLUMN nickname text; COMMIT;\n' > \ "$repo/apps/api/migrations/0002_transaction.sql" commit_all "$repo" transaction expect_fail "$repo" "$base" 'transaction control operation' repo=$(new_repo filename) base=$(git -C "$repo" rev-parse HEAD) printf 'SELECT 1;\n' >"$repo/apps/api/migrations/next.SQL" commit_all "$repo" filename expect_fail "$repo" "$base" 'new migration must use NNNN_lowercase_name.sql' repo=$(new_repo ordering) base=$(git -C "$repo" rev-parse HEAD) printf 'SELECT 1;\n' >"$repo/apps/api/migrations/0000_late.sql" commit_all "$repo" ordering expect_fail "$repo" "$base" 'new migration name must sort after 0001_init.sql' repo=$(new_repo nested) base=$(git -C "$repo" rev-parse HEAD) mkdir -p "$repo/apps/api/migrations/archive" printf 'SELECT 1;\n' >"$repo/apps/api/migrations/archive/0002_hidden.sql" commit_all "$repo" nested expect_fail "$repo" "$base" 'new migration must use NNNN_lowercase_name.sql' repo=$(new_repo symlink) base=$(git -C "$repo" rev-parse HEAD) printf 'SELECT 1;\n' >"$repo/outside.sql" ln -s ../../../outside.sql "$repo/apps/api/migrations/0002_symlink.sql" commit_all "$repo" symlink expect_fail "$repo" "$base" 'new migration must be a mode 100644 regular file' repo=$(new_repo ancestry) base=$(git -C "$repo" rev-parse HEAD) git -C "$repo" switch -q --orphan unrelated printf 'unrelated\n' >"$repo/unrelated" git -C "$repo" add unrelated git -C "$repo" commit -qm unrelated expect_fail "$repo" "$base" 'release base is not an ancestor of HEAD' repo=$(new_repo invalid_sha) expect_fail "$repo" deadbeef 'must be a full lowercase commit SHA' echo 'migration_tests=PASS'