fix: heal migration-ledger mismatch and make Azure SSO deploy-proof (#18)
PocketBase on Labs crash-looped since the SSO deploy (PR #17): mounting --migrationsDir for the first time replayed the entire migration history against a database that was provisioned out-of-band (empty _migrations ledger) and died on 1778948471_created_content.js. On top of that the team_members->auth migration had its own crash paths and trapped OAuth2 config inside a one-shot, env-dependent migration. - pb_migrations/1000000000_baseline_ledger_sync.js: detects an out-of-band provisioned DB (schema exists, ledger empty) and marks the 79 historical migrations as applied; no-op on fresh or already-synced DBs - pb_migrations/1781000000_team_members_to_auth.js: idempotency guard, relation->text conversion WITH data preservation (PocketBase diffs fields by id, so the column is backed up and restored via SQL), unique index rebuild, no silent catches, env only as fast-path - pb_hooks/entra_oidc.pb.js + pb_hooks/utils.js: reconcile the Entra OIDC provider from ENTRA_* env on every bootstrap + cron tick (compare-before-save, warn-once); heals environments that migrated without secrets and supports secret rotation without re-apply - pb_hooks/team_members.pb.js: require() pattern — JSVM runs callbacks as isolated programs, top-level helpers are not in scope (adopts Leroy's fix from the fix-sso branch) - infra/*/site/deploy-playbook.yml: health-gate after compose up — the deploy fails loudly with container logs when PocketBase does not become healthy (runs #83-#88 were green while PB crash-looped) - docker-compose.yml: .env.local is optional again - docs/auth-spec.md + AI_AGENT.md: ledger/reconciler documentation, ADRs 006-008, never-rename-applied-migrations warning Verified locally against PocketBase v0.30.4 with a 7-scenario DoD matrix (fresh DB +/- env, out-of-band DB with data incl. data preservation, populated-ledger upgrade, late-secrets healing, re-run guard, hook provisioning): 15/15 pass. npm test 112/112. Closes #18 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
138
pb_migrations/1000000000_baseline_ledger_sync.js
Normal file
138
pb_migrations/1000000000_baseline_ledger_sync.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
//
|
||||
// Issue #18 — Baseline ledger sync for out-of-band provisioned databases.
|
||||
//
|
||||
// The Labs/production PocketBase originally ran WITHOUT --migrationsDir: its
|
||||
// schema was created by scripts/setup-pb-collections.mjs / the admin UI, so its
|
||||
// _migrations ledger is empty. When the migrations dir was mounted for the
|
||||
// first time (Azure SSO, PR #17), PocketBase tried to replay the entire
|
||||
// migration history against that existing schema and crash-looped on the very
|
||||
// first file ("Collection name must be unique").
|
||||
//
|
||||
// This migration sorts before every other file. When it detects that the
|
||||
// legacy schema already exists but the ledger has never tracked it, it marks
|
||||
// the historical migrations below as applied so the replay is skipped and the
|
||||
// chain continues with the genuinely new migrations (team_members_to_auth,
|
||||
// tighten_api_rules). On fresh databases and on databases that already have a
|
||||
// populated ledger it is a no-op.
|
||||
//
|
||||
// NOTE for future agents: never rename a migration file after it has been
|
||||
// applied anywhere — the ledger is filename-based. New migrations must sort
|
||||
// after 1781100001 and be appended to environments through a normal deploy.
|
||||
const HISTORICAL_MIGRATIONS = [
|
||||
"1778948471_created_content.js",
|
||||
"1778948471_created_quiz_banks.js",
|
||||
"1778948471_created_relations.js",
|
||||
"1778948471_created_sources.js",
|
||||
"1778948471_created_team_members.js",
|
||||
"1778948471_created_topics.js",
|
||||
"1778948472_created_leaderboard.js",
|
||||
"1778948472_created_learn_progress.js",
|
||||
"1778948472_created_quiz_cache.js",
|
||||
"1778948472_created_quiz_results.js",
|
||||
"1778948472_created_settings.js",
|
||||
"1778954289_created_test_col2.js",
|
||||
"1778954310_deleted_relations.js",
|
||||
"1778954310_deleted_topics.js",
|
||||
"1778954310_deleted_users.js",
|
||||
"1778954311_deleted_content.js",
|
||||
"1778954311_deleted_leaderboard.js",
|
||||
"1778954311_deleted_learn_progress.js",
|
||||
"1778954311_deleted_quiz_banks.js",
|
||||
"1778954311_deleted_quiz_cache.js",
|
||||
"1778954311_deleted_quiz_results.js",
|
||||
"1778954311_deleted_settings.js",
|
||||
"1778954311_deleted_sources.js",
|
||||
"1778954311_deleted_team_members.js",
|
||||
"1778954311_deleted_test_col2.js",
|
||||
"1778954317_created_content.js",
|
||||
"1778954317_created_leaderboard.js",
|
||||
"1778954317_created_learn_progress.js",
|
||||
"1778954317_created_quiz_banks.js",
|
||||
"1778954317_created_quiz_cache.js",
|
||||
"1778954317_created_quiz_results.js",
|
||||
"1778954317_created_relations.js",
|
||||
"1778954317_created_settings.js",
|
||||
"1778954317_created_sources.js",
|
||||
"1778954317_created_team_members.js",
|
||||
"1778954317_created_topics.js",
|
||||
"1779005271_created_test_col3.js",
|
||||
"1779005289_created_test_col4.js",
|
||||
"1779005309_deleted_content.js",
|
||||
"1779005309_deleted_learn_progress.js",
|
||||
"1779005309_deleted_quiz_banks.js",
|
||||
"1779005309_deleted_quiz_cache.js",
|
||||
"1779005309_deleted_quiz_results.js",
|
||||
"1779005309_deleted_relations.js",
|
||||
"1779005309_deleted_sources.js",
|
||||
"1779005309_deleted_team_members.js",
|
||||
"1779005309_deleted_topics.js",
|
||||
"1779005310_deleted_leaderboard.js",
|
||||
"1779005310_deleted_settings.js",
|
||||
"1779005310_deleted_test_col3.js",
|
||||
"1779005310_deleted_test_col4.js",
|
||||
"1779005316_created_content.js",
|
||||
"1779005316_created_quiz_banks.js",
|
||||
"1779005316_created_relations.js",
|
||||
"1779005316_created_sources.js",
|
||||
"1779005316_created_topics.js",
|
||||
"1779005317_created_leaderboard.js",
|
||||
"1779005317_created_learn_progress.js",
|
||||
"1779005317_created_quiz_cache.js",
|
||||
"1779005317_created_quiz_results.js",
|
||||
"1779005317_created_settings.js",
|
||||
"1779005317_created_team_members.js",
|
||||
"1779127586_created_curriculum.js",
|
||||
"1779127759_collections_snapshot.js",
|
||||
"1779200000_updated_sources.js",
|
||||
"1780000001_updated_topics.js",
|
||||
"1780500000_updated_topics_relevance_locked.js",
|
||||
"1780500001_normalize_relation_types.js",
|
||||
"1780500002_created_llm_calls.js",
|
||||
"1780600000_curriculum_v2.js",
|
||||
"1780700000_sources_progress.js",
|
||||
"1780800000_created_micro_learnings.js",
|
||||
"1780800001_deleted_legacy_collections.js",
|
||||
"1780800002_update_micro_learnings_rules.js",
|
||||
"1780900000_team_members_enrollment.js",
|
||||
"1780900001_created_test_results.js",
|
||||
"1781000000_created_theme_sessions.js",
|
||||
"1781100000_created_question_bank.js",
|
||||
"1781100001_created_on_demand_attempts.js",
|
||||
];
|
||||
|
||||
migrate((app) => {
|
||||
// Fresh database? (no legacy schema) -> let the normal chain run.
|
||||
try {
|
||||
app.findCollectionByNameOrId("content");
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ledger already tracks the history? -> normally migrated DB, nothing to do.
|
||||
const probe = new DynamicModel({ count: 0 });
|
||||
app.db()
|
||||
.newQuery("SELECT COUNT(*) AS count FROM _migrations WHERE file = {:file}")
|
||||
.bind({ file: "1778948471_created_content.js" })
|
||||
.one(probe);
|
||||
if (probe.count > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Out-of-band provisioned database: schema exists, ledger is empty.
|
||||
// Mark the historical migrations as applied so they are not replayed.
|
||||
const applied = Date.now();
|
||||
for (const file of HISTORICAL_MIGRATIONS) {
|
||||
app.db()
|
||||
.newQuery("INSERT OR IGNORE INTO _migrations (file, applied) VALUES ({:file}, {:applied})")
|
||||
.bind({ file: file, applied: applied })
|
||||
.execute();
|
||||
}
|
||||
console.log(
|
||||
"baseline_ledger_sync: detected out-of-band provisioned database — marked " +
|
||||
HISTORICAL_MIGRATIONS.length + " historical migrations as applied"
|
||||
);
|
||||
}, (_app) => {
|
||||
// Down: intentionally a no-op. The ledger rows describe environment-specific
|
||||
// bookkeeping; removing them would re-trigger the replay this fix prevents.
|
||||
});
|
||||
Reference in New Issue
Block a user