From 2315a54154c45a5e6dfca8cad24b6562e10087e7 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Tue, 28 Jul 2026 22:07:41 +0200 Subject: [PATCH] ci: add protection on merge and db migration this is a protection to prevent the merge of any branch adding a database migration with an timestamp earlier than the current targetted branch --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++++++++++++++++++++ justfile | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 961b49ad..ea7d42c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: backend: ${{ steps.filter.outputs.backend }} wasm: ${{ steps.filter.outputs.wasm }} plugins: ${{ steps.filter.outputs.plugins }} + migrations: ${{ steps.filter.outputs.migrations }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 @@ -49,6 +50,8 @@ jobs: - 'src/application/ports/plugin_ports.rs' - 'src/application/adapters/plugin_lifecycle_hook.rs' - 'src/application/adapters/plugin_user_lifecycle_hook.rs' + migrations: + - 'migrations/**' frontend-check: name: Frontend — svelte-check, ESLint, Stylelint, Prettier @@ -77,6 +80,59 @@ jobs: - name: Unit tests run: npm run test:unit + # Fails the PR if a new sqlx migration file has a timestamp NOT strictly + # greater than every migration already on the target branch. Guards + # against the "two branches in flight, whoever merges second breaks + # every deployment" case: sqlx's default strict mode rejects an + # `_sqlx_migrations` row inserted with a timestamp older than one + # already applied. Same logic as `just check-migrations`. + migration-ordering: + name: Migration ordering (new migrations postdate target branch) + needs: changes + if: needs.changes.outputs.migrations == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch with full history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch target branch + run: git fetch --quiet origin "$BASE_REF" + env: + BASE_REF: ${{ github.base_ref }} + - name: Verify ordering + env: + BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + base_max=$(git ls-tree -r "origin/${BASE_REF}" --name-only -- migrations/ \ + | grep -oE 'migrations/[0-9]{14}_' \ + | sed 's|migrations/||; s|_$||' \ + | sort | tail -1) + if [[ -z "$base_max" ]]; then + echo "No migrations on origin/${BASE_REF} — skipping ordering check." + exit 0 + fi + new=$(git diff --name-only --diff-filter=A "origin/${BASE_REF}...HEAD" -- migrations/ \ + | grep -E 'migrations/[0-9]{14}_' || true) + if [[ -z "$new" ]]; then + echo "No new migrations on this branch — nothing to check." + exit 0 + fi + fail=0 + for m in $new; do + ts=$(basename "$m" | grep -oE '^[0-9]{14}') + if [[ "$ts" -le "$base_max" ]]; then + # `::error` surfaces on the file in the PR diff view. + echo "::error file=$m::Migration timestamp $ts is not strictly > origin/${BASE_REF}'s latest ($base_max). Rename to > $base_max to avoid sqlx strict-mode errors on deploy." + fail=1 + fi + done + if [[ $fail == 0 ]]; then + echo "OK — all new migrations post-date origin/${BASE_REF}'s latest ($base_max)." + fi + exit $fail + rust-fmt: name: Rustfmt needs: changes diff --git a/justfile b/justfile index 1682b237..da721de0 100644 --- a/justfile +++ b/justfile @@ -98,6 +98,52 @@ check: cargo clippy --all-features --all-targets -- -D warnings +# Verify every new sqlx migration this branch introduces has a +# timestamp STRICTLY greater than any migration already on the target +# branch (default: main). Guards against the "PR is fine at creation +# but got overtaken by a later-timestamped migration merging first" +# case that would trip sqlx strict-mode on deploy — and the local case +# where you rebased and now your migration file is out-of-order vs. +# what your dev DB already applied. +# +# Usage: +# just check-migrations # compares against origin/main +# just check-migrations dev # compares against origin/dev +# +# Same logic runs in `.github/workflows/migrations.yml` on every PR. +check-migrations base='main': + #!/usr/bin/env bash + set -euo pipefail + git fetch --quiet origin {{base}} + base_max=$(git ls-tree -r "origin/{{base}}" --name-only -- migrations/ \ + | grep -oE 'migrations/[0-9]{14}_' \ + | sed 's|migrations/||; s|_$||' \ + | sort | tail -1) + if [[ -z "$base_max" ]]; then + echo "No migrations on origin/{{base}} — skipping ordering check." + exit 0 + fi + new=$(git diff --name-only --diff-filter=A "origin/{{base}}...HEAD" -- migrations/ \ + | grep -E 'migrations/[0-9]{14}_' || true) + if [[ -z "$new" ]]; then + echo "No new migrations on this branch — nothing to check." + exit 0 + fi + fail=0 + for m in $new; do + ts=$(basename "$m" | grep -oE '^[0-9]{14}') + if [[ "$ts" -le "$base_max" ]]; then + echo "ERROR: $m has timestamp $ts, not strictly > origin/{{base}}'s latest ($base_max)." + echo " Rename to a timestamp > $base_max to avoid sqlx strict-mode errors on deploy." + fail=1 + fi + done + if [[ $fail == 0 ]]; then + echo "OK — all new migrations post-date origin/{{base}}'s latest ($base_max)." + fi + exit $fail + + wasm-check: cd wasm/oxicloud-hash; cargo fmt --all cd wasm/oxicloud-hash; cargo clippy --all-features --release -- -D warnings