migraguard 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -8
- package/dist/cli.js +7 -6
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Execution is deliberately simple: plain SQL files executed via `psql`. migraguar
|
|
|
19
19
|
- **Tamper detection in CI (offline)** — Only the tail file (linear) or leaf nodes (DAG) are editable. `check` rejects changes to any other file without DB connection
|
|
20
20
|
- **Regression detection** — If a hotfixed file reverts to an old checksum, `apply` raises an error immediately
|
|
21
21
|
- **Failure blocking with explicit resolve** — A `failed` migration blocks all progress until a human explicitly judges and resolves it
|
|
22
|
-
- **Drift gate + Idempotency proof** — two [verification mechanisms](#verification-two-distinct-mechanisms): `apply --with-drift-check` detects schema divergence before applying; `verify` proves migrations are safely re-executable on a shadow DB
|
|
22
|
+
- **Drift gate + Idempotency proof** — two [verification mechanisms](#verification-two-distinct-mechanisms): `apply --with-drift-check` detects local schema divergence before applying; `diff` verifies post-deploy schema consistency; `verify` proves migrations are safely re-executable on a shadow DB
|
|
23
23
|
- **Mutual exclusion** — `apply` uses PostgreSQL advisory locks to prevent concurrent execution
|
|
24
24
|
- **One release at a time** — the next migration cannot be added until the current release is deployed to all environments, ensuring the latest file is always hotfix-ready
|
|
25
25
|
|
|
@@ -65,6 +65,23 @@ migraguard separates file integrity and application state into two layers.
|
|
|
65
65
|
|
|
66
66
|
metadata.json represents "which files should exist"; schema_migrations represents "what has been applied." This separation enables correct staged rollout from a single repository to multiple environments (staging, production).
|
|
67
67
|
|
|
68
|
+
### Source of Truth: migrations (SSoT) vs schema.sql
|
|
69
|
+
|
|
70
|
+
migraguard treats **migration SQL files** as the **Single Source of Truth (SSoT)** for schema evolution.
|
|
71
|
+
They capture not only the end state, but also the *intent, ordering, and operational safety tactics* required for production changes.
|
|
72
|
+
|
|
73
|
+
`schema.sql` is a **derived artifact**:
|
|
74
|
+
- Generated from a real database via `dump` (pg_dump), and updated locally by `apply --with-drift-check`
|
|
75
|
+
- Used as an **expected-state snapshot** for drift detection (`diff`) and human review
|
|
76
|
+
- Not intended to be hand-edited or treated as the authoritative desired state
|
|
77
|
+
|
|
78
|
+
This design supports migraguard's incident-prevention model:
|
|
79
|
+
- Offline CI integrity checks (`check`) can reason about history and editability rules without a DB
|
|
80
|
+
- Regression detection can catch "hotfix reversion" back to a previous checksum
|
|
81
|
+
- Drift is treated as a deployment blocker unless explicitly resolved through the normal workflow
|
|
82
|
+
|
|
83
|
+
> If you prefer a "desired state" workflow where the schema definition itself is the SSoT and migrations are generated from it, consider tools like Atlas. migraguard is optimized for teams writing DDL directly with operational guardrails.
|
|
84
|
+
|
|
68
85
|
### Checksum Normalization
|
|
69
86
|
|
|
70
87
|
Checksums are computed on **normalized SQL** (SHA-256): comments are stripped (`-- ...` and `/* ... */` including nested), whitespace is collapsed, string literals are preserved as-is. Adding comments, adjusting indentation, or inserting blank lines does not change the checksum; only actual SQL statement changes are detected. `-- migraguard:depends-on` directives are also comments and do not affect the checksum.
|
|
@@ -90,10 +107,11 @@ The table is **fully INSERT-only** — no UPDATEs. Every application attempt (in
|
|
|
90
107
|
|
|
91
108
|
| Mechanism | Purpose | When to use |
|
|
92
109
|
|-----------|---------|-------------|
|
|
93
|
-
| `apply --with-drift-check` | **
|
|
110
|
+
| `apply --with-drift-check` | **Local drift gate**: detect unauthorized schema changes before apply, auto-update dump after | Local development before commit |
|
|
111
|
+
| `diff` | **Post-deploy verification**: confirm DB schema matches expected `schema.sql` after apply | CI pipeline on merge to release branches (run after `apply`) |
|
|
94
112
|
| `verify` | **Idempotency proof**: apply migrations twice on a shadow DB, confirm no errors and no schema change | Before releases or in CI as a final safety net |
|
|
95
113
|
|
|
96
|
-
`apply --with-drift-check` guards against drift; `verify` proves re-executability.
|
|
114
|
+
`apply --with-drift-check` guards against drift in local development; `diff` verifies schema consistency after deployment; `verify` proves re-executability. All are stronger than lint rules — they operate on actual DB state. See [docs/state-model.md](docs/state-model.md) for detailed flows.
|
|
97
115
|
|
|
98
116
|
## Workflow
|
|
99
117
|
|
|
@@ -117,8 +135,8 @@ CI (PR):
|
|
|
117
135
|
migraguard verify (optional) → idempotency proof on shadow DB
|
|
118
136
|
|
|
119
137
|
Deploy:
|
|
120
|
-
merge to db_dev → CI: apply
|
|
121
|
-
merge to db_pro → CI: apply
|
|
138
|
+
merge to db_dev → CI: apply → diff → staging
|
|
139
|
+
merge to db_pro → CI: apply → diff → production
|
|
122
140
|
```
|
|
123
141
|
|
|
124
142
|
**Key rule**: Do not add the next migration file until the current release is deployed to all environments. This ensures the latest file can always be modified and re-applied for hotfixes.
|
|
@@ -150,7 +168,7 @@ See [docs/state-model.md](docs/state-model.md) for detailed apply, check, resolv
|
|
|
150
168
|
| `new <name>` | Generate a new migration SQL file |
|
|
151
169
|
| `squash` | Merge pending files into one for release |
|
|
152
170
|
| `apply` | Execute pending migrations via `psql` |
|
|
153
|
-
| `apply --with-drift-check` |
|
|
171
|
+
| `apply --with-drift-check` | Local: drift check → apply → dump update |
|
|
154
172
|
| `resolve <file>` | Mark a failed migration as skipped (explicit judgment) |
|
|
155
173
|
| `status` | Display migration status per file |
|
|
156
174
|
| `editable` | List currently editable files (tail / leaf) |
|
|
@@ -207,7 +225,13 @@ jobs:
|
|
|
207
225
|
node-version: '20'
|
|
208
226
|
- run: npm ci
|
|
209
227
|
- run: npx migraguard check
|
|
210
|
-
- run: npx migraguard apply
|
|
228
|
+
- run: npx migraguard apply
|
|
229
|
+
env:
|
|
230
|
+
PGHOST: ${{ secrets.DB_HOST }}
|
|
231
|
+
PGDATABASE: ${{ secrets.DB_NAME }}
|
|
232
|
+
PGUSER: ${{ secrets.DB_USER }}
|
|
233
|
+
PGPASSWORD: ${{ secrets.DB_PASSWORD }}
|
|
234
|
+
- run: npx migraguard diff
|
|
211
235
|
env:
|
|
212
236
|
PGHOST: ${{ secrets.DB_HOST }}
|
|
213
237
|
PGDATABASE: ${{ secrets.DB_NAME }}
|
|
@@ -219,6 +243,7 @@ jobs:
|
|
|
219
243
|
|
|
220
244
|
```json
|
|
221
245
|
{
|
|
246
|
+
"model": "dag",
|
|
222
247
|
"migrationsDirs": ["db/migrations"],
|
|
223
248
|
"schemaFile": "db/schema.sql",
|
|
224
249
|
"metadataFile": "db/.migraguard/metadata.json",
|
|
@@ -251,6 +276,12 @@ jobs:
|
|
|
251
276
|
}
|
|
252
277
|
```
|
|
253
278
|
|
|
279
|
+
### Model Configuration
|
|
280
|
+
|
|
281
|
+
| Key | Default | Description |
|
|
282
|
+
|-----|---------|-------------|
|
|
283
|
+
| `model` | _(unset = linear)_ | Set to `"dag"` to enable DAG mode. When set in config, takes precedence over `metadata.json` |
|
|
284
|
+
|
|
254
285
|
### Naming Configuration
|
|
255
286
|
|
|
256
287
|
| Key | Default | Description |
|
|
@@ -422,7 +453,7 @@ migraguard embeds operational policies into the tool and prevents incidents via
|
|
|
422
453
|
|------|-----------|---------|-------|--------|------------------|
|
|
423
454
|
| **Tamper detection** | checksum + CI gate (offline) | checksum (at apply time) | Merkle hash (atlas.sum) | Merkle tree (sqitch.plan) | none |
|
|
424
455
|
| **Regression detection** | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
425
|
-
| **Drift detection** | ✅ apply --with-drift-check | ❌ | ✅ schema diff | ❌ | ⚠️ |
|
|
456
|
+
| **Drift detection** | ✅ apply --with-drift-check (local) / diff (CI) | ❌ | ✅ schema diff | ❌ | ⚠️ |
|
|
426
457
|
| **Idempotency verification** | ✅ verify (double-apply) | ❌ | ❌ | ❌ | ❌ |
|
|
427
458
|
| **Parallel releases** | ✅ DAG | ❌ | ❌ | ⚠️ | ❌ |
|
|
428
459
|
| **Offline CI gate** | ✅ check | ❌ | ✅ atlas.sum | ❌ | ❌ |
|
package/dist/cli.js
CHANGED
|
@@ -112,6 +112,7 @@ function buildConfig(raw, configDir) {
|
|
|
112
112
|
};
|
|
113
113
|
return {
|
|
114
114
|
configDir,
|
|
115
|
+
...raw.model ? { model: raw.model } : {},
|
|
115
116
|
migrationsDirs: resolveMigrationsDirs(raw),
|
|
116
117
|
schemaFile: raw.schemaFile ?? "db/schema.sql",
|
|
117
118
|
metadataFile: raw.metadataFile ?? "db/.migraguard/metadata.json",
|
|
@@ -342,8 +343,8 @@ function addEntry(metadata, entry) {
|
|
|
342
343
|
migrations: [...metadata.migrations, entry]
|
|
343
344
|
};
|
|
344
345
|
}
|
|
345
|
-
function isDagMode(metadata) {
|
|
346
|
-
return metadata.model === "dag";
|
|
346
|
+
function isDagMode(metadata, config) {
|
|
347
|
+
return config?.model === "dag" || metadata.model === "dag";
|
|
347
348
|
}
|
|
348
349
|
function isPreModelSince(metadata, fileName) {
|
|
349
350
|
if (!metadata.model || !metadata.modelSince) return true;
|
|
@@ -863,7 +864,7 @@ async function commandCheck(config) {
|
|
|
863
864
|
const errors = [];
|
|
864
865
|
const metadata = await loadMetadata(config);
|
|
865
866
|
const files = await scanMigrations(config);
|
|
866
|
-
const dag = isDagMode(metadata);
|
|
867
|
+
const dag = isDagMode(metadata, config);
|
|
867
868
|
const metadataMap = new Map(metadata.migrations.map((m) => [m.file, m.checksum]));
|
|
868
869
|
const recordedFiles = files.filter((f) => metadataMap.has(f.fileName));
|
|
869
870
|
const newFiles = files.filter((f) => !metadataMap.has(f.fileName));
|
|
@@ -973,7 +974,7 @@ async function commandSquash(config) {
|
|
|
973
974
|
console.log(chalk6.yellow("No new migration files to squash."));
|
|
974
975
|
return;
|
|
975
976
|
}
|
|
976
|
-
if (isDagMode(metadata)) {
|
|
977
|
+
if (isDagMode(metadata, config)) {
|
|
977
978
|
await dagSquash(config, newFiles, metadata);
|
|
978
979
|
} else {
|
|
979
980
|
if (newFiles.length === 1) {
|
|
@@ -2198,7 +2199,7 @@ async function commandEditable(config) {
|
|
|
2198
2199
|
return { editableFiles: [], entries: [] };
|
|
2199
2200
|
}
|
|
2200
2201
|
const metadata = await loadMetadata(config);
|
|
2201
|
-
const dag = isDagMode(metadata);
|
|
2202
|
+
const dag = isDagMode(metadata, config);
|
|
2202
2203
|
const metadataFileSet = new Set(metadata.migrations.map((m) => m.file));
|
|
2203
2204
|
const newFiles = files.filter((f) => !metadataFileSet.has(f.fileName));
|
|
2204
2205
|
const entries = [];
|
|
@@ -2391,7 +2392,7 @@ async function commandApply(config, options) {
|
|
|
2391
2392
|
}
|
|
2392
2393
|
}
|
|
2393
2394
|
const metadata = await loadMetadata(config);
|
|
2394
|
-
const dag = isDagMode(metadata);
|
|
2395
|
+
const dag = isDagMode(metadata, config);
|
|
2395
2396
|
let graph = null;
|
|
2396
2397
|
let leafSet = null;
|
|
2397
2398
|
if (dag) {
|