ci: enforce production-safe database migrations
ci / verify (pull_request) Successful in 8m50s

This commit is contained in:
2026-07-17 16:59:50 +08:00
parent 74c20b1976
commit f226f9c953
9 changed files with 863 additions and 5 deletions
+39 -1
View File
@@ -10,22 +10,45 @@ runner_config=$root/deploy/ci/act-runner-v2-config.yaml
provision=$root/scripts/provision-ci-runner.sh
build_images=$root/scripts/ci-build-images.sh
validate_semver=$root/scripts/ci-validate-semver.sh
validate_migrations=$root/scripts/ci-validate-migrations.mjs
build_images_test=$root/tests/ci/ci-build-images-test.sh
migrations_test=$root/tests/ci/migrations-test.sh
migration_base=$root/deploy/ci/production-migration-base
adr=$root/docs/decisions/001-production-cicd.md
runbook=$root/docs/runbooks/production-ci-cd.md
migrator=$root/apps/api/cmd/migrate/main.go
for file in "$workflow" "$release_workflow" "$runner_service" "$runner_config" "$provision" "$build_images" "$validate_semver" "$build_images_test" "$adr" "$runbook"; do
for file in "$workflow" "$release_workflow" "$runner_service" "$runner_config" "$provision" "$build_images" "$validate_semver" "$validate_migrations" "$build_images_test" "$migrations_test" "$migration_base" "$adr" "$runbook" "$migrator"; do
[[ -f $file ]] || {
printf 'missing CI/CD file: %s\n' "$file" >&2
exit 1
}
done
if [[ $(wc -l <"$migration_base") -ne 1 ]]; then
echo 'production migration baseline must contain exactly one line' >&2
exit 1
fi
grep -Fq 'SET standard_conforming_strings = on' "$migrator" || {
echo 'database migrator does not pin standard SQL string semantics' >&2
exit 1
}
if ! grep -Eq '^[0-9a-f]{40}$' "$migration_base"; then
echo 'production migration baseline must be a full lowercase SHA' >&2
exit 1
fi
grep -q '^name: ci$' "$workflow"
grep -q '^ push:$' "$workflow"
grep -q '^ branches: \[main\]$' "$workflow"
grep -q '^ pull_request:$' "$workflow"
grep -q '^ verify:$' "$workflow"
grep -Fq 'git checkout --detach "$CI_SHA"' "$workflow"
grep -Fq 'test "$(git rev-parse HEAD)" = "$CI_SHA"' "$workflow"
if grep -Fq 'git checkout --detach FETCH_HEAD' "$workflow"; then
echo 'main/PR CI must not checkout mutable FETCH_HEAD after comparison fetches' >&2
exit 1
fi
if grep -Eq "^[[:space:]]+tags:|Verify release tag ancestry" "$workflow" || \
grep -Fq './scripts/ci-validate-semver.sh "$tag_name"' "$workflow"; then
echo 'main/PR CI must not share the release-tag context' >&2
@@ -53,6 +76,7 @@ for quality_workflow in "$workflow" "$release_workflow"; do
fi
grep -Fq 'test ! -f .git/shallow' "$quality_workflow"
grep -Fq './tests/ci/ci-build-images-test.sh' "$quality_workflow"
grep -Fq './tests/ci/migrations-test.sh' "$quality_workflow"
grep -Fq './tests/ci/pipeline-test.sh' "$quality_workflow"
grep -Fq './tests/ci/semver-test.sh' "$quality_workflow"
grep -Fq 'docker-compose version' "$quality_workflow"
@@ -81,6 +105,7 @@ for quality_workflow in "$workflow" "$release_workflow"; do
fi
for gate in \
'scripts/ci-validate-migrations.mjs' \
'gofmt -l' \
'go vet ./...' \
'go test ./...' \
@@ -103,6 +128,12 @@ for quality_workflow in "$workflow" "$release_workflow"; do
"$quality_workflow" >&2
exit 1
}
grep -Fq 'production_base=$(cat deploy/ci/production-migration-base)' \
"$quality_workflow" || {
printf '%s does not read the versioned production migration baseline\n' \
"$quality_workflow" >&2
exit 1
}
done
if [[ $(sed -n '/^ - name: Verify Go formatting$/,$p' "$workflow") != \
@@ -111,6 +142,13 @@ if [[ $(sed -n '/^ - name: Verify Go formatting$/,$p' "$workflow") != \
exit 1
fi
grep -Fq 'CI_EVENT_BEFORE: ${{ github.event.before }}' "$workflow"
grep -Fq 'CI_PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}' "$workflow"
grep -Fq 'git merge-base --is-ancestor "$immutable_base" HEAD' "$workflow"
grep -Fq '"$production_base" "$immutable_base"' "$workflow"
grep -Fq 'node ./scripts/ci-validate-migrations.mjs "$production_base"' \
"$release_workflow"
runner_image='docker.io/gitea/runner:2.0.0-dind-rootless@sha256:5b7b625ff773d0ee761788c47582503ec1b241fa5b81edebad48a57e663f4f3a'
job_image='docker.io/library/node:24.16.0-bookworm@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7'