diff --git a/docs/runbooks/production-ci-cd.md b/docs/runbooks/production-ci-cd.md index cee74b0..f3e7a9c 100644 --- a/docs/runbooks/production-ci-cd.md +++ b/docs/runbooks/production-ci-cd.md @@ -95,7 +95,7 @@ rootless DinD 不是 VM 沙箱。外层仍按官方要求使用 `--privileged` ## 数据库迁移兼容门禁 -`deploy/ci/production-migration-base` 必须等于当前生产 API 对应的完整源码 SHA。`scripts/ci-validate-migrations.mjs` 只允许新增、按文件名严格追加且为普通文件的迁移;生产已知迁移以及已经进入 `main` 的迁移不可修改或删除。DROP、TRUNCATE、DELETE、MERGE、CREATE OR REPLACE、动态 EXECUTE、RENAME、列类型变更、增加/设置 `NOT NULL`、删除默认值和 REVOKE 会直接使 CI 失败。 +`deploy/ci/production-migration-base` 必须等于当前生产 API 对应的完整源码 SHA。`scripts/ci-validate-migrations.mjs` 只允许新增、按文件名严格追加且为普通文件的迁移;生产已知迁移以及已经进入 `main` 的迁移不可修改或删除。DROP、TRUNCATE、DELETE、MERGE、CREATE OR REPLACE、动态 EXECUTE、事务控制、DO/函数/过程定义、RENAME、列类型变更、增加/设置 `NOT NULL`、删除默认值和 REVOKE 会直接使 CI 失败。需要过程式迁移时先拆成可审计的声明式 expand/contract SQL;不能把过程体藏在普通或 dollar-quoted 字符串中。 该规则允许 INSERT/UPDATE 等数据迁移,因此不能替代备份和恢复验证。每次生产发布成功并完成 `pg_restore` 演练后,创建一个只前移 `deploy/ci/production-migration-base` 的受审 PR;不要把基线指向尚未成功部署的提交。 diff --git a/scripts/ci-validate-migrations.mjs b/scripts/ci-validate-migrations.mjs index 0911cc0..b75b309 100755 --- a/scripts/ci-validate-migrations.mjs +++ b/scripts/ci-validate-migrations.mjs @@ -192,6 +192,11 @@ function executableSql(sql, source = 'SQL input') { } const destructiveRules = [ + { + name: 'procedural SQL body', + pattern: + /^\s*(?:DO\b|CREATE\s+(?:OR\s+REPLACE\s+)?(?:FUNCTION|PROCEDURE)\b)/i, + }, { name: 'dynamic SQL execution', pattern: /\bEXECUTE\b/i, diff --git a/tests/ci/migrations-test.sh b/tests/ci/migrations-test.sh index 1988660..816ce25 100755 --- a/tests/ci/migrations-test.sh +++ b/tests/ci/migrations-test.sh @@ -165,7 +165,15 @@ END; $$; SQL commit_all "$repo" plpgsql_block_end -expect_pass "$repo" "$base" +expect_fail "$repo" "$base" 'procedural SQL body' + +repo=$(new_repo ordinary_string_procedural_body) +base=$(git -C "$repo" rev-parse HEAD) +cat >"$repo/apps/api/migrations/0002_do_string.sql" <<'SQL' +DO LANGUAGE plpgsql 'BEGIN DROP TABLE accounts; END'; +SQL +commit_all "$repo" ordinary_string_procedural_body +expect_fail "$repo" "$base" 'procedural SQL body' repo=$(new_repo ordinary_string_boundary) base=$(git -C "$repo" rev-parse HEAD)