mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
fix(apply-migrations): stop reporting 'All migrations up to date' while schema is behind (#1530)
The pre-flight detected schema-version drift but only warned; the orchestrator path then printed 'All migrations up to date.' and exited 0. Now --yes/--non-interactive runs the schema migrations during the pre-flight (engine already connected), and interactive runs exit 1 with a pointer to --yes / --force-schema instead of the false all-clear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0612b0daa8
commit
2bfbb4104d
@@ -108,7 +108,7 @@ Flags:
|
||||
|
||||
Exit codes:
|
||||
0 Success (including "nothing to do").
|
||||
1 An orchestrator failed.
|
||||
1 An orchestrator failed, or schema migrations are pending (re-run with --yes).
|
||||
2 Invalid arguments.
|
||||
`);
|
||||
}
|
||||
@@ -259,6 +259,41 @@ function printDryRun(plan: Plan, installed: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #1530: schema-drift pre-flight resolution. When the schema version is
|
||||
* behind, `--yes`/`--non-interactive` runs the schema migrations right there
|
||||
* (the engine is already connected); interactive runs warn and return true so
|
||||
* the caller exits non-zero instead of claiming "All migrations up to date".
|
||||
* All output goes to stderr (migrations never print to stdout).
|
||||
*
|
||||
* Returns true when the schema is STILL behind after this call.
|
||||
*/
|
||||
async function resolveSchemaBehind(opts: {
|
||||
schemaVer: number;
|
||||
latest: number;
|
||||
autoApply: boolean;
|
||||
run: () => Promise<{ applied: number; current: number }>;
|
||||
}): Promise<boolean> {
|
||||
const { schemaVer, latest, autoApply, run } = opts;
|
||||
if (schemaVer >= latest) return false;
|
||||
if (autoApply) {
|
||||
console.error(`Schema version ${schemaVer} is behind latest ${latest}; running schema migrations...`);
|
||||
try {
|
||||
const result = await run();
|
||||
console.error(`Applied ${result.applied} schema migration(s); now at v${result.current}.`);
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error(`Schema migration failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
console.warn(
|
||||
`\n⚠️ Schema version ${schemaVer} is behind latest ${latest}.\n` +
|
||||
` Run \`gbrain apply-migrations --yes\` to apply now, or \`gbrain init --migrate-only\`.\n`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function orchestratorOptsFrom(cli: ApplyMigrationsArgs): OrchestratorOpts {
|
||||
return {
|
||||
yes: cli.yes || cli.nonInteractive,
|
||||
@@ -354,10 +389,13 @@ export async function runApplyMigrations(args: string[]): Promise<void> {
|
||||
if (cli.forceAll) return; // both surfaces flushed
|
||||
}
|
||||
|
||||
// Pre-flight: warn if schema migrations (migrate.ts) are behind.
|
||||
// apply-migrations runs orchestrator migrations only; schema migrations
|
||||
// run via connectEngine() / initSchema(). Users often expect this CLI
|
||||
// to handle everything (Issue 1 from v0.18.0 field report).
|
||||
// Pre-flight: detect schema migrations (migrate.ts) being behind.
|
||||
// apply-migrations historically ran orchestrator migrations only; schema
|
||||
// migrations run via connectEngine() / initSchema(). Users expect this CLI
|
||||
// to handle everything (Issue 1 from v0.18.0 field report; #1530). With
|
||||
// --yes/--non-interactive we apply them here; otherwise we warn and make
|
||||
// sure the run does NOT report "All migrations up to date" with exit 0.
|
||||
let schemaBehind = false;
|
||||
try {
|
||||
const { LATEST_VERSION } = await import('../core/migrate.ts');
|
||||
const { loadConfig: lc, toEngineConfig } = await import('../core/config.ts');
|
||||
@@ -377,14 +415,14 @@ export async function runApplyMigrations(args: string[]): Promise<void> {
|
||||
await eng.connect(toEngineConfig(cfg));
|
||||
const verStr = await eng.getConfig('version');
|
||||
const schemaVer = parseInt(verStr || '1', 10);
|
||||
const { runMigrations } = await import('../core/migrate.ts');
|
||||
schemaBehind = await resolveSchemaBehind({
|
||||
schemaVer,
|
||||
latest: LATEST_VERSION,
|
||||
autoApply: (cli.yes || cli.nonInteractive) && !cli.dryRun,
|
||||
run: () => runMigrations(eng),
|
||||
});
|
||||
await eng.disconnect();
|
||||
if (schemaVer < LATEST_VERSION) {
|
||||
console.warn(
|
||||
`\n⚠️ Schema version ${schemaVer} is behind latest ${LATEST_VERSION}.\n` +
|
||||
` Schema migrations run automatically on next connectEngine() / initSchema().\n` +
|
||||
` To run them now: gbrain init --migrate-only\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -419,6 +457,13 @@ export async function runApplyMigrations(args: string[]): Promise<void> {
|
||||
|
||||
const toRun: Migration[] = [...plan.partial, ...plan.pending];
|
||||
if (toRun.length === 0) {
|
||||
if (schemaBehind) {
|
||||
console.error(
|
||||
'Orchestrator migrations are up to date, but schema migrations are behind. ' +
|
||||
'Run `gbrain apply-migrations --yes` (or `--force-schema`) to apply them.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('All migrations up to date.');
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -503,4 +548,5 @@ export const __testing = {
|
||||
buildPlan,
|
||||
indexCompleted,
|
||||
statusForVersion,
|
||||
resolveSchemaBehind,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { describe, test, expect } from 'bun:test';
|
||||
import { __testing } from '../src/commands/apply-migrations.ts';
|
||||
import type { CompletedMigrationEntry } from '../src/core/preferences.ts';
|
||||
|
||||
const { parseArgs, indexCompleted, buildPlan, statusForVersion } = __testing;
|
||||
const { parseArgs, indexCompleted, buildPlan, statusForVersion, resolveSchemaBehind } = __testing;
|
||||
|
||||
describe('parseArgs', () => {
|
||||
test('default flags', () => {
|
||||
@@ -180,3 +180,60 @@ describe('runApplyMigrations exit codes (v0.36.1.x #1062)', () => {
|
||||
expect(src).toMatch(/All migrations up to date[\s\S]{0,80}process\.exit\(0\)/);
|
||||
});
|
||||
});
|
||||
|
||||
// #1530: apply-migrations must not report "All migrations up to date" (exit 0)
|
||||
// while the SCHEMA is behind. --yes runs the schema migrations in the
|
||||
// pre-flight; interactive runs flag schemaBehind and exit 1.
|
||||
describe('resolveSchemaBehind (#1530)', () => {
|
||||
test('schema up to date → false, migrations not run', async () => {
|
||||
let ran = false;
|
||||
const behind = await resolveSchemaBehind({
|
||||
schemaVer: 5,
|
||||
latest: 5,
|
||||
autoApply: true,
|
||||
run: async () => { ran = true; return { applied: 0, current: 5 }; },
|
||||
});
|
||||
expect(behind).toBe(false);
|
||||
expect(ran).toBe(false);
|
||||
});
|
||||
|
||||
test('behind + autoApply → runs schema migrations, no longer behind', async () => {
|
||||
let ran = false;
|
||||
const behind = await resolveSchemaBehind({
|
||||
schemaVer: 3,
|
||||
latest: 5,
|
||||
autoApply: true,
|
||||
run: async () => { ran = true; return { applied: 2, current: 5 }; },
|
||||
});
|
||||
expect(behind).toBe(false);
|
||||
expect(ran).toBe(true);
|
||||
});
|
||||
|
||||
test('behind + interactive → warns and stays behind, migrations not run', async () => {
|
||||
let ran = false;
|
||||
const behind = await resolveSchemaBehind({
|
||||
schemaVer: 3,
|
||||
latest: 5,
|
||||
autoApply: false,
|
||||
run: async () => { ran = true; return { applied: 2, current: 5 }; },
|
||||
});
|
||||
expect(behind).toBe(true);
|
||||
expect(ran).toBe(false);
|
||||
});
|
||||
|
||||
test('behind + autoApply + migration failure → stays behind', async () => {
|
||||
const behind = await resolveSchemaBehind({
|
||||
schemaVer: 3,
|
||||
latest: 5,
|
||||
autoApply: true,
|
||||
run: async () => { throw new Error('boom'); },
|
||||
});
|
||||
expect(behind).toBe(true);
|
||||
});
|
||||
|
||||
test('up-to-date branch exits 1 when schemaBehind (source shape)', async () => {
|
||||
const { readFileSync } = await import('fs');
|
||||
const src = readFileSync('src/commands/apply-migrations.ts', 'utf8');
|
||||
expect(src).toMatch(/if \(schemaBehind\)[\s\S]{0,300}process\.exit\(1\)[\s\S]{0,120}All migrations up to date/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user