Skip to content

fix: never seed the migration baseline into a fresh database - #686

Merged
sakibsadmanshajib merged 3 commits into
mainfrom
fix/migration-baseline-fresh-database
Aug 3, 2026
Merged

sakibsadmanshajib merged 3 commits into
mainfrom
fix/migration-baseline-fresh-database

Conversation

@sakibsadmanshajib

@sakibsadmanshajib sakibsadmanshajib commented Aug 1, 2026 •

Copy link
Copy Markdown
Owner

Fixes #676.

The defect

scripts/apply-migrations.sh seeded scripts/migration-baseline.conf into public.hive_schema_migrations whenever that ledger was empty. An empty ledger is also exactly the state of a brand new database, so on a fresh install the script marked 61 of the 84 files in supabase/migrations/ as already applied without executing one of them, then continued against schema that was never created and reported success.

That is precisely the state the demo box cutover to a self-hosted Supabase will be in, on a host reachable only through a self-hosted runner, where a silent 54 to 61 file skip is close to unrecoverable without a teardown and looks like a green deploy.

Why the baseline still exists

It prevents re-running migrations on a database that predates the ledger, whose history was applied by hand and recorded nowhere. That purpose is untouched. The bug was only that "the ledger is empty" cannot distinguish that database from a brand new one.

How the two are told apart

By the schema, not by the ledger. scripts/probe-applied-migrations.py already probes object by object, so it grows one mode rather than a second mechanism:

scripts/probe-applied-migrations.py --baseline-state

It reports one of three answers.

state meaning what the applier does
fresh not one owned object of any baseline migration exists does not seed, leaves the whole chain pending
preexisting every baseline migration's owned objects are all present seeds the baseline, exactly as before
ambiguous anything else, including a baseline gone stale against this database aborts, records nothing, applies nothing, prints what is needed

Unknown resolves to not applied, never to applied. A probe that cannot run, a missing python3, or an unparseable answer all land in ambiguous and abort.

Extensions are excluded from the decision on purpose. supabase/postgres ships pgcrypto and uuid-ossp pre-created and deploy/supabase/init/00-extensions.sql adds vector before any migration runs, so counting an extension as evidence would make a brand new database look partly migrated and abort the very install this guard protects.

Both the shell script and the probe parse the baseline file. The probe now also prints baseline_entries=N and the shell aborts if that disagrees with its own count, so the two parsers cannot drift apart silently.

The operator flag is belt and braces, not the guard

HIVE_MIGRATION_BASELINE=seed|ignore is honoured only where the probe is inconclusive or could not run, and is rejected outright when it contradicts the schema. The failure mode of a required flag is someone forgetting it, so it is never the only thing standing between a fresh database and a silent skip.

Verification

Real throwaway Postgres, supabase/postgres:17.6.1.136, the image the cutover uses. A plain postgres or pgvector image lacks the supabase_auth_admin role, whose absence makes 20260516_07 fail outright. One scratch database per scenario, checked in as scripts/test-apply-migrations.sh and wired to a path gated workflow.

migration files: 84, baseline entries: 61, remainder: 23

=== an override that contradicts a fresh schema is rejected
  ok   exit 1 (non-zero)
  ok   output mentions 'contradicts the schema'
  ok   ledger rows = 0
  ok   public tables = 0

=== an empty database applies the FULL chain
  ok   exit status = 0
  ok   output mentions 'baseline_state=fresh'
  ok   output mentions 'the baseline is NOT seeded'
  ok   executed per ledger = 84
  ok   recorded as baseline without running = 0
  ok   ledger rows = 84
  ok   reported applied count = applied 84 migration(s)
  ok   the schema really exists = t

=== a populated ledger missing one file applies exactly that file
  ok   exit status = 0
  ok   output does not mention 'baseline_state='
  ok   output does not mention 'seeding'
  ok   reported applied count = applied 1 migration(s)
  ok   ledger rows = 84

=== a database that predates the ledger seeds the baseline and applies the rest
  ok   exit status = 0
  ok   output mentions 'baseline_state=preexisting'
  ok   pending count = 23
  ok   still nothing recorded by a dry run = 0

=== a populated ledger is authoritative and the schema is never probed
  ok   exit status = 0
  ok   output does not mention 'baseline_state='
  ok   output does not mention 'seeding'
  ok   report = no pending migrations (84 files, all recorded)
  ok   ledger rows = 84
  ok   nothing executed (public tables) = 0

=== a half applied schema with an empty ledger aborts
  ok   exit 1 (non-zero)
  ok   output mentions 'baseline_state=ambiguous'
  ok   output mentions 'cannot tell whether'
  ok   output mentions 'HIVE_MIGRATION_BASELINE=seed'
  ok   ledger rows = 0
  ok   nothing further executed = t

=== an operator override resolves the ambiguous state
  ok   exit status = 0
  ok   output mentions 'on the operator's instruction'
  ok   pending count = 23
  ok   still nothing recorded = 0

=== a run that cannot probe the schema refuses to guess
  ok   exit 1 (non-zero)
  ok   output mentions 'the schema probe failed'
  ok   output mentions 'cannot tell whether'
  ok   still nothing recorded = 0

=== a bad override value is rejected
  ok   exit 1 (non-zero)
  ok   output mentions 'must be seed or ignore'
  ok   still nothing recorded = 0

PASS: every scenario held

The stated bound is counted, not inferred from an exit code. Against an empty database the ledger ends with 84 rows of source=applied, zero of source=baseline, and the run's own last line reports 84 executed, equal to ls supabase/migrations/*.sql | wc -l.

How the live database is protected

The live demo database has a populated ledger, so decide_baseline is never reached there and the code path is byte for byte the one that runs today. Two scenarios pin that down rather than asserting it:

  • "a populated ledger is authoritative and the schema is never probed": the ledger holds all 84 filenames, the run reports no pending migrations, baseline_state= never appears in the output, and no migration executes.
  • "a populated ledger missing one file applies exactly that file": the everyday deploy shape. One file is deleted from the ledger, exactly one migration runs, the probe is still never consulted.

Additionally verified out of band, not in the checked in suite because it costs a second full chain: fully migrating a database, truncating the ledger and re-running produces baseline_state=preexisting, seeds 61 rows and then executes the 23 remaining files cleanly against a database that already has them, ending with 61 baseline rows and 23 applied rows and exit 0. So the ledger rebuild path on the live database completes rather than aborting.

Per statement idempotency is untouched. No migration file's contents changed, and neither the ledger schema nor its filename key changed.

Finding, out of scope for this fix

A genuine fresh chain replay needs GoTrue to have run first. deploy/supabase/init/00-extensions.sql creates the auth schema and the anon, authenticated and service_role roles, but not auth.users, auth.uid() or auth.jwt(), which GoTrue owns. Confirmed empirically on supabase/postgres:17.6.1.136: with only that init file applied, the chain fails at the very first file, 20260328_01_identity_foundation.sql, with relation "auth.users" does not exist. Thirteen migrations foreign key to auth.users and ten call auth.uid() or auth.jwt().

The cutover therefore has to start GoTrue against the new database before running the applier, or provide those objects some other way. This PR is scoped to the baseline logic and does not attempt to solve bootstrap ordering. The test harness creates the same stand in objects GoTrue would, which is also what .github/ci/test-db-bootstrap.sql does for CI.

Also in this PR

  • The provenance block in migration-baseline.conf claimed a probe of "all 79 files" while the repository now holds 84. Corrected to say it covered the 79 that existed at probe time, with the five newer files pending by absence.
  • The new Migration applier tests workflow is path gated at the workflow level so an unrelated pull request never pays for the Postgres image. It is not a required check; it should earn that after a track record on main.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented fresh databases from being incorrectly marked as migrated without executing migrations.
    • Migration setup now distinguishes fresh, preexisting, and ambiguous database states.
    • Ambiguous or inconsistent states now stop deployment instead of risking data integrity.
  • Improvements

    • Added validated baseline options for supported preexisting databases.
    • Dry runs now follow the same migration and baseline decisions as live runs.
  • Documentation

    • Clarified migration baseline requirements, schema detection, and handling of newly added migrations.

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sakibsadmanshajib, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d1210403-a00f-44fb-a075-a742179f7a79

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca929b and ff8f678.

📒 Files selected for processing (3)
  • scripts/apply-migrations.sh
  • scripts/probe-applied-migrations.py
  • scripts/test-apply-migrations.sh
📝 Walkthrough

Walkthrough

The migration applier now detects database schema state before baseline seeding. Fresh databases execute all migrations, preexisting databases can use the baseline, and ambiguous states stop unless explicitly overridden. A disposable PostgreSQL workflow validates these scenarios.

Changes

Migration baseline handling

Layer / File(s) Summary
Schema probe and baseline contract
scripts/probe-applied-migrations.py, scripts/migration-baseline.conf
The probe validates baseline entries, counts migration-owned objects, excludes extensions, and reports fresh, preexisting, or ambiguous states.
Conditional baseline application
scripts/apply-migrations.sh, .github/workflows/deploy-demo-box.yml
The applier uses schema detection for apply and dry-run modes. It seeds only eligible preexisting databases and documents ambiguous-state deployment failures.
End-to-end regression coverage
scripts/test-apply-migrations.sh, .wolf/buglog.jsonl
The integration harness tests fresh, populated, ambiguous, override, dry-run, probe-failure, and invalid-override scenarios.
Regression workflow wiring
.github/workflows/migration-applier-tests.yml
A path-gated workflow provisions a disposable Supabase PostgreSQL instance and runs the migration-applier tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant apply-migrations.sh
  participant probe-applied-migrations.py
  participant PostgreSQL
  Operator->>apply-migrations.sh: Run migration apply or dry run
  apply-migrations.sh->>probe-applied-migrations.py: Request baseline state
  probe-applied-migrations.py->>PostgreSQL: Inspect schema catalog
  PostgreSQL-->>probe-applied-migrations.py: Return owned-object evidence
  probe-applied-migrations.py-->>apply-migrations.sh: Return database state
  apply-migrations.sh->>PostgreSQL: Seed baseline or execute pending migrations
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: preventing migration baseline seeding into fresh databases.
Linked Issues check ✅ Passed The PR addresses the migration and CI rehearsal prerequisites from [#676] by ensuring fresh databases replay all migrations.
Out of Scope Changes check ✅ Passed The workflow, detection logic, documentation, bug log, and integration tests directly support the migration objectives in [#676].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/migration-baseline-fresh-database

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Independent review: MERGE WITH NITS

Verified against a throwaway supabase/postgres:17.6.1.136, not from the builder's report. Every number below was counted independently rather than read from the script's own log line.

The three-state boundary holds, including realistic partial states

The builder's harness exercises the partial case at one migration deep. I ran it at six depths, applying the first N baseline migrations to simulate a run that died halfway:

baseline migrations applied probe state applier exit ledger rows
1 of 61 ambiguous 1 0
15 of 61 ambiguous 1 0
30 of 61 ambiguous 1 0
45 of 61 ambiguous 1 0
60 of 61 ambiguous 1 0
61 of 61 preexisting 0 84

The boundary is exact. Sixty of sixty one still refuses, and only the complete set flips to preexisting. No depth resolved to fresh or preexisting by accident.

ambiguous records nothing. The ledger table is created by the DDL step that runs first, then the decision aborts with zero rows in it. Re-running after an operator finishes the remaining baseline files by hand yields preexisting and settles correctly at 61 baseline rows plus 23 executed, totalling 84.

The override cannot beat a confident probe

HIVE_MIGRATION_BASELINE=seed against a fresh schema exits 1 with "contradicts the schema", and ignore against a preexisting schema does the same. An unset variable on an inconclusive state aborts rather than seeding, so absence is never read as permission. An unparseable probe result routes to the same hard abort.

The live path is unreachable, verified in code and in a container

decide_baseline is called only under count(*) = 0 in apply mode (scripts/apply-migrations.sh:340) and only under an empty applied array in dry-run mode (line 367). Against a populated 84 row ledger over a deliberately empty schema, the most hostile shape available, the probe never ran and nothing was seeded under all four override values including an invalid one. The demo database is therefore unaffected.

Fail loud

No || true, set +e, continue-on-error, or stderr suppression in any changed file. The baseline_entries disagreement guard (line 284) is skipped only when the probe produced no parseable output at all, and that path already forces state=unknown into the hard abort branch, so skipping the guard cannot become a bypass.

The count: 84

Counted three ways rather than trusting the summary line. Eighty four distinct filenames in the run's ::group::applying lines, 71 public tables created from a starting count of zero, and 84 ledger rows all carrying source='applied' with zero source='baseline'. I also recomputed sha256 for all 84 files and matched each against its ledger row, with no mismatches.

GoTrue prerequisite confirmed

With deploy/supabase/init/00-extensions.sql applied and no GoTrue stand-ins, the chain dies immediately: exit 3 at 20260328_01_identity_foundation.sql with relation "auth.users" does not exist. The init script leaves the auth schema and the anon, authenticated, service_role and supabase_auth_admin roles in place, but auth.users, auth.uid() and auth.jwt() are all absent. Recorded on #676, since the bootstrap order there has to be extensions and roles, then GoTrue's own migrations, then this chain.

No migration file content changed, and the ledger schema and its filename key are untouched.

One claim in the builder's report is wrong, and it matters

The report says .github/ci/test-db-bootstrap.sql does a bare CREATE ROLE authenticated and supabase_auth_admin, "which errors on the supabase image, meaning it had never been run". The first half is true, the inference is not. That file is used at .github/workflows/ci.yml:361, inside the go-tests job, which is a required check, and that lane runs pgvector/pgvector:pg17. On a plain Postgres image those roles do not pre-exist, so the bare CREATE ROLE succeeds. The file is correct for its own lane, and that lane is neither silently broken nor silently skipped. It is also consumed by docs/proof/tenant-model-entitlement/run-proof.sh:36. Rejecting the harness for supabase/postgres was still the right call; only the reason needs correcting so nobody later deletes a working bootstrap on the strength of it.

Nits, none blocking

  1. HIVE_MIGRATION_BASELINE=maybe is not rejected when the ledger is populated, because the value check lives inside decide_baseline and that function is never reached. Harmless, since the variable has no meaning on that path, but the validation reads as global and is not.
  2. scripts/probe-applied-migrations.py is missing from the paths filter in .github/workflows/deploy-demo-box.yml, even though the migrate step now hard depends on it. A probe only change will not retrigger the deploy. migration-applier-tests.yml does cover it, so the gap is narrow.
  3. The two parsers of migration-baseline.conf count differently: the probe uses len(set(baseline)) while the shell uses the raw array length. A duplicated applied = line would trip the disagreement guard and abort the deploy. There are no duplicates today, 61 equals 61, and the abort is the safe direction, so this is a note rather than a defect.
  4. The probe needs python3. The migrate job is ubuntu-latest, so it is present today. Whichever runner performs the Migrate Hive off Supabase Cloud onto self-hosted Supabase on the demo box #676 cutover needs it too, otherwise the run hard aborts, which is again the safe direction.

Merge state

GitHub reports this branch as CONFLICTING / DIRTY. It needs a rebase before it can merge, most likely on .wolf/buglog.jsonl, which conflicts serially by design. The new workflow is correctly not a required check.

The defect in #676 is genuinely fixed and the fix fails in the safe direction at every boundary I could construct. Merge once the conflict is resolved.

scripts/apply-migrations.sh seeded scripts/migration-baseline.conf into
public.hive_schema_migrations whenever that ledger was empty. An empty ledger is
also exactly the state of a brand new database, so a fresh install recorded 61
of the 84 migration files as already applied without executing one of them, then
deployed against schema that was never created and reported success. That is the
state the demo box cutover to a self-hosted Supabase will be in, on a host
reachable only through a self-hosted runner, where a silent skip is close to
unrecoverable without a teardown.

An empty ledger has two opposite meanings and looks identical in both: a
database that predates the ledger and ran those migrations by hand, or a new
database that has run nothing. The ledger cannot tell them apart, so the schema
is asked instead. probe-applied-migrations.py grows a --baseline-state mode that
reports fresh (not one owned object of any baseline migration exists),
preexisting (every baseline migration's owned objects are all present) or
ambiguous (anything else, including a baseline gone stale against the database
in front of it). Fresh skips the seed and leaves the whole chain pending;
preexisting seeds exactly as before; ambiguous aborts, records nothing, applies
nothing and prints what an operator needs to do. Unknown resolves to not
applied, never to applied.

Extensions are excluded from that decision on purpose. supabase/postgres ships
pgcrypto and uuid-ossp pre-created and deploy/supabase/init/00-extensions.sql
adds vector before any migration runs, so counting an extension as evidence
would make a brand new database look partly migrated and abort the very install
the guard protects.

HIVE_MIGRATION_BASELINE=seed|ignore is belt and braces rather than the guard
itself, because the failure mode of a required flag is someone forgetting it. It
is honoured only where the probe is inconclusive or could not run, and rejected
outright when it contradicts the schema. A run that cannot execute the probe
refuses to guess. Both this script and the probe parse the baseline file, so the
probe now reports how many entries it read and the script aborts if the two
counts disagree.

scripts/test-apply-migrations.sh proves the behaviour against a real throwaway
supabase/postgres 17.6.1.136, the image the cutover uses, one scratch database
per scenario. The bound is counted, not inferred from an exit code: against an
empty database the ledger ends with 84 rows of source=applied, zero of
source=baseline, and the run reports 84 executed, equal to the number of files
in supabase/migrations. A populated ledger behaves exactly as before, the probe
is never consulted, and a ledger missing one file applies exactly that file. A
half applied schema with an empty ledger aborts.

Related finding, out of scope here: a genuine fresh chain replay needs GoTrue to
have run first. deploy/supabase/init/00-extensions.sql creates the auth schema
and the anon, authenticated and service_role roles but not auth.users,
auth.uid() or auth.jwt(), so on a bare self-hosted database the chain fails at
the first file, 20260328_01_identity_foundation.sql, on a missing auth.users.
That is a bootstrap ordering constraint, not a baseline one.

Refs #676
A probe-only change did not retrigger the migrate step's paths filter
even though apply-migrations.sh now hard depends on the probe script.
Adds scripts/probe-applied-migrations.py alongside the existing
apply-migrations.sh and migration-baseline.conf entries so an edit to
the probe is not a silent no-deploy.
@sakibsadmanshajib
sakibsadmanshajib force-pushed the fix/migration-baseline-fresh-database branch from 692b369 to 2ca929b Compare August 3, 2026 12:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
scripts/test-apply-migrations.sh (1)

128-135: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove each scenario log file.

run_applier calls mktemp on every invocation and never removes the previous file. The suite runs the applier about ten times, so it leaves about ten files in the temp directory. Add a cleanup trap.

♻️ Proposed change
 log=""
 status=0
+trap 'rm -f "$log"' EXIT
 run_applier() { # run_applier <db> [args...]; combined output in $log, exit in $status
   local db="$1"; shift
+  [ -n "$log" ] && rm -f "$log"
   log="$(mktemp)"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-apply-migrations.sh` around lines 128 - 135, Update run_applier
to clean up the previous temporary log file before replacing log with a new
mktemp path, and add an EXIT cleanup trap for the final log file so every
scenario log is removed when the script finishes.
.github/workflows/migration-applier-tests.yml (1)

70-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer break over exit 0 in the readiness loop, and dump logs when the tests fail.

Line 73 uses exit 0 to leave the readiness loop. It ends the whole step, so the current behavior is correct, but break states the intent and keeps the step extensible. Separately, the run step at line 88 has no failure diagnostics. A failed scenario currently leaves no Postgres log in the run summary.

♻️ Proposed change
           for _ in $(seq 1 60); do
             if docker exec migdb pg_isready -U postgres -h 127.0.0.1 >/dev/null 2>&1; then
               echo "postgres is accepting connections"
-              exit 0
+              break
             fi
             sleep 2
           done
-          echo "::error::Postgres never became ready"
-          docker logs migdb | tail -40
-          exit 1
+          if ! docker exec migdb pg_isready -U postgres -h 127.0.0.1 >/dev/null 2>&1; then
+            echo "::error::Postgres never became ready"
+            docker logs migdb | tail -40
+            exit 1
+          fi

Add a diagnostics step after the scenario step:

      - name: Dump Postgres logs on failure
        if: failure()
        run: docker logs migdb | tail -200
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/migration-applier-tests.yml around lines 70 - 79, Update
the readiness loop around the docker exec check to use break instead of exit 0,
allowing the step to continue after Postgres becomes ready. Add a post-scenario
workflow step named “Dump Postgres logs on failure” that runs only when
failure() is true and outputs the final Postgres logs with docker logs migdb |
tail -200.
scripts/apply-migrations.sh (1)

256-262: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validate HIVE_MIGRATION_BASELINE before the ledger check.

decide_baseline validates the override value, and the script calls decide_baseline only when the ledger is empty. On a database with a populated ledger, an invalid value such as HIVE_MIGRATION_BASELINE=maybe passes without any message. The operator then believes the override took effect. Validate the value once, near the start of the run, and keep the decision logic where it is.

♻️ Proposed change
 baseline_decision=""
+
+# Validated on every run, not only on the empty-ledger path, so a typo is
+# reported even when the ledger makes the override irrelevant.
+case "${HIVE_MIGRATION_BASELINE:-}" in
+  ""|seed|ignore) ;;
+  *) echo "::error::HIVE_MIGRATION_BASELINE must be seed or ignore, got: ${HIVE_MIGRATION_BASELINE}"; exit 1 ;;
+esac
+
 decide_baseline() {
   local override="${HIVE_MIGRATION_BASELINE:-}"
-  case "$override" in
-    ""|seed|ignore) ;;
-    *) echo "::error::HIVE_MIGRATION_BASELINE must be seed or ignore, got: $override"; exit 1 ;;
-  esac

Also applies to: 335-341

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/apply-migrations.sh` around lines 256 - 262, Validate
HIVE_MIGRATION_BASELINE near the start of the script run, before the ledger
check, so invalid values fail consistently even when the ledger is populated.
Reuse the existing validation logic from decide_baseline without invoking the
baseline decision early; keep decide_baseline in its current location for
selecting the baseline when the ledger is empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/probe-applied-migrations.py`:
- Around line 253-265: Align baseline counting across scripts by defining one
duplicate-handling convention in load_baseline and documenting it, then ensure
the probe’s baseline_entries output uses that convention. In
scripts/probe-applied-migrations.py lines 253-265, update load_baseline and its
count accordingly; in scripts/test-apply-migrations.sh lines 70-71, derive
baseline_count from the probe’s baseline_entries output rather than the
exact-spacing grep, preserving consistent handling of duplicates and whitespace.

In `@scripts/test-apply-migrations.sh`:
- Around line 143-144: Update the affected run_applier calls in the scenarios
around “an override that contradicts a fresh schema is rejected” and the
referenced later cases so HIVE_MIGRATION_BASELINE and the PATH shim are
explicitly set or cleared immediately before each invocation. Ensure each call
receives only its intended temporary environment and cannot inherit values left
by a previous run_applier call.

---

Nitpick comments:
In @.github/workflows/migration-applier-tests.yml:
- Around line 70-79: Update the readiness loop around the docker exec check to
use break instead of exit 0, allowing the step to continue after Postgres
becomes ready. Add a post-scenario workflow step named “Dump Postgres logs on
failure” that runs only when failure() is true and outputs the final Postgres
logs with docker logs migdb | tail -200.

In `@scripts/apply-migrations.sh`:
- Around line 256-262: Validate HIVE_MIGRATION_BASELINE near the start of the
script run, before the ledger check, so invalid values fail consistently even
when the ledger is populated. Reuse the existing validation logic from
decide_baseline without invoking the baseline decision early; keep
decide_baseline in its current location for selecting the baseline when the
ledger is empty.

In `@scripts/test-apply-migrations.sh`:
- Around line 128-135: Update run_applier to clean up the previous temporary log
file before replacing log with a new mktemp path, and add an EXIT cleanup trap
for the final log file so every scenario log is removed when the script
finishes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0063ef8f-5729-4610-bafa-b8c148838ba7

📥 Commits

Reviewing files that changed from the base of the PR and between d0bdec0 and 2ca929b.

📒 Files selected for processing (7)
  • .github/workflows/deploy-demo-box.yml
  • .github/workflows/migration-applier-tests.yml
  • .wolf/buglog.jsonl
  • scripts/apply-migrations.sh
  • scripts/migration-baseline.conf
  • scripts/probe-applied-migrations.py
  • scripts/test-apply-migrations.sh

Comment thread scripts/probe-applied-migrations.py Outdated
Comment thread scripts/test-apply-migrations.sh Outdated
Both points come from the CodeRabbit review on PR #686.

The prefix assignments before run_applier were assignments in the harness shell,
not in the applier's environment, because run_applier is a shell function rather
than an external command. Measured, in posix mode (how bash behaves when invoked
as sh) bash 4.4 and 5.0 leave the variable set after the function returns while
bash 5.1 and later do not, so HIVE_MIGRATION_BASELINE or the python3 PATH shim
could carry from one scenario into every later one. run_applier now takes leading
VAR=value words and hands them to env(1), which cannot leak, and every call site
passes them that way.

No scenario outcome changes. A leak is loud rather than silent here: the fresh
database scenario runs immediately after the seed override and would be rejected
for contradicting the schema, so a passing run was already proof that nothing
leaked. The full harness against supabase/postgres:17.6.1.136 reports the same
nine scenarios green after the change.

The three readers of scripts/migration-baseline.conf now share one convention:
comments start at a #, surrounding whitespace is insignificant, and an applied
entry appears exactly ONCE, so a raw count and a de-duplicated count are the same
number. The applier and the probe each reject a duplicate by name, the probe
counts the raw list rather than a set, and the harness takes its count from the
applier's own --check output instead of a spacing sensitive grep, echoing that
output on failure so the message naming the bad line is not swallowed. A
duplicated or differently spaced entry is now one clear error instead of three
disagreeing totals and a parsers disagree abort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sakibsadmanshajib
sakibsadmanshajib merged commit 946d66e into main Aug 3, 2026
17 checks passed
@github-actions
github-actions Bot deleted the fix/migration-baseline-fresh-database branch August 3, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate Hive off Supabase Cloud onto self-hosted Supabase on the demo box

1 participant