删除 Gitea Actions、Tag/Main 自动流水线和旧 Runner 配置,取消 Git 操作与发布授权的绑定。\n\n新增本地镜像发布、固定生产部署助手、digest manifest、迁移安全检查、simulation 冒烟及显式回滚流程。\n\n验证:pnpm lint、pnpm test、pnpm build、Go 全量测试、ShellCheck、Compose 配置、人工发布测试和 linux/amd64 完整栈冒烟。
139 lines
4.5 KiB
Bash
Executable File
139 lines
4.5 KiB
Bash
Executable File
#!/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'
|