Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Migration safety

CodeDecay analyzes repo-local PostgreSQL migration SQL as a **plan-only** safety
check. It does not connect to a database or apply migrations.

## What it can establish

- Operation classification (add/drop/rename/alter/index/backfill)
- Rolling-deploy blockers for destructive or rename operations
- NOT NULL without default/backfill as a static blocker
- Five-state deployment matrix statuses
- Connection-target classification (`localhost` vs production-looking hosts)
- Cleanup obligations for disposable targets (plan recorded, not executed)
- Verdicts: `plan-ready`, `plan-blocked`, `needs-execution-proof`, `not-fully-verified`

## What it cannot establish

- Existing-data compatibility
- Lock duration / live rollback success
- Mixed-version application behavior
- A `fullyVerified: true` result (always false in this slice)

## CLI / MCP

```bash
codedecay migration --file migration.sql --target-kind disposable-local --cleanup-plan "drop volume codedecay-mig"
codedecay migration --file migration.sql --connection-host db.rds.amazonaws.com
```

MCP tool: `migration_safety`.

Prisma schema-diff planning remains available through
`createPrismaMigrationAdapterPlan` (read-only `prisma migrate diff`); applying
migrations is still blocked by CodeDecay execution safety.
12 changes: 11 additions & 1 deletion packages/cli/src/commands/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ export function runMigrationCommand(context: CliCommandContext, dependencies: Ru
const options = parseMigrationArgs(context.args);
const cwd = resolve(context.runtimeCwd, options.cwd ?? ".");
const rootDir = dependencies.resolveRepoRoot(cwd, options);
const report = analyzeMigrationSafety({ rootDir, files: options.files, rollbackFiles: options.rollbackFiles, targetKind: options.targetKind });
const report = analyzeMigrationSafety({
rootDir,
files: options.files,
rollbackFiles: options.rollbackFiles,
targetKind: options.targetKind,
connectionUrl: options.connectionUrl,
connectionHost: options.connectionHost,
databaseUrlEnv: options.databaseUrlEnv,
cleanupPlan: options.cleanupPlan,
rollbackFailed: options.rollbackFailed
});
const rendered = options.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderMigrationSafetyMarkdown(report);
dependencies.writeOutput({ cwd: rootDir, output: options.output, rendered, runtime: context.runtime });
}
15 changes: 13 additions & 2 deletions packages/cli/src/docs/command-docs/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@ export const ANALYSIS_COMMAND_DOCS: Record<string, CommandDoc> = {
{ flag: "--file <path>", description: "Repo-local migration SQL file; repeat for multiple files" },
{ flag: "--rollback-file <path>", description: "Repo-local rollback SQL file; repeat for multiple files" },
{ flag: "--target-kind <kind>", description: "unspecified, disposable-local, remote-unapproved, or production-like" },
{ flag: "--connection-url <url>", description: "Optional DB URL used only for host classification; secret values are redacted" },
{ flag: "--connection-host <host>", description: "Optional DB host used for target classification" },
{ flag: "--database-url-env <NAME>", description: "Env var name holding credentials; values are never read" },
{ flag: "--cleanup-plan <text>", description: "Disposable database cleanup plan recorded in the report" },
{ flag: "--rollback-failed", description: "Mark rollback as failed so the verdict stays not fully verified" },
{ flag: "--cwd <path>", description: "Repository working directory (default: current directory)" },
{ flag: "--format <format>", description: "json or markdown (default: markdown)" },
{ flag: "--output <path>", description: "Write the plan to a file instead of stdout" }
],
examples: ["codedecay migration --file prisma/migrations/20260802_change/migration.sql --target-kind disposable-local", "codedecay migration --file migration.sql --target-kind production-like --format json"],
notes: ["This command is plan-only: it reads no database secret, contacts no database, and applies no migration."]
examples: [
"codedecay migration --file prisma/migrations/20260802_change/migration.sql --target-kind disposable-local",
"codedecay migration --file migration.sql --connection-host localhost --cleanup-plan \"drop docker volume codedecay-mig\" --format json"
],
notes: [
"This command is plan-only: it reads no database secret, contacts no database, and applies no migration.",
"See docs/migration.md for what plan-ready vs fully-verified means."
]
},
runtime: {
name: "runtime",
Expand Down
13 changes: 12 additions & 1 deletion packages/cli/src/parsers/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export function parseMigrationArgs(args: string[]): MigrationOptions {
const arg = args[index];
if (!arg) continue;
if (arg === "--help" || arg === "-h") throw new HelpRequested();
if (arg === "--rollback-failed") {
options.rollbackFailed = true;
continue;
}
const [flag, inline] = splitArg(arg);
const value = () => inline ?? requireValue(args, index, flag);
if (flag === "--file") options.files.push(value());
Expand All @@ -16,7 +20,14 @@ export function parseMigrationArgs(args: string[]): MigrationOptions {
else if (flag === "--output") options.output = value();
else if (flag === "--format") options.format = parseFormat(value());
else if (flag === "--target-kind") options.targetKind = parseTarget(value());
else { throwUnknownOption(arg, "migration"); continue; }
else if (flag === "--connection-url") options.connectionUrl = value();
else if (flag === "--connection-host") options.connectionHost = value();
else if (flag === "--database-url-env") options.databaseUrlEnv = value();
else if (flag === "--cleanup-plan") options.cleanupPlan = value();
else {
throwUnknownOption(arg, "migration");
continue;
}
if (inline === undefined) index += 1;
}
return options;
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/types/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ export interface MigrationOptions {
targetKind: MigrationTargetKind;
format: ConfigFormat;
output?: string | undefined;
connectionUrl?: string | undefined;
connectionHost?: string | undefined;
databaseUrlEnv?: string | undefined;
cleanupPlan?: string | undefined;
rollbackFailed?: boolean | undefined;
}
7 changes: 6 additions & 1 deletion packages/knowledge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,20 @@ export {
export { ingestRuntimeEvidence, persistRuntimeEvidenceArtifact, RUNTIME_EVIDENCE_ARTIFACT_PATH } from "./runtime/ingest";
export { analyzeMigrationSafety } from "./migration/analyze";
export type { AnalyzeMigrationSafetyOptions } from "./migration/analyze";
export { classifyMigrationConnectionTarget } from "./migration/target-safety";
export { renderMigrationSafetyMarkdown } from "./migration/render";
export { MIGRATION_EVIDENCE_SCHEMA_VERSION } from "./migration/types";
export type {
MigrationCleanupEvidence,
MigrationConnectionTarget,
MigrationMatrixState,
MigrationOperationEvidence,
MigrationOperationKind,
MigrationRisk,
MigrationRollbackStatus,
MigrationSafetyReport,
MigrationTargetKind
MigrationTargetKind,
MigrationVerdict
} from "./migration/types";
export type { IngestRuntimeEvidenceOptions } from "./runtime/ingest";
export { renderRuntimeEvidenceMarkdown } from "./runtime/render";
Expand Down
Loading
Loading