You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Update: round-trip withOverrides config and redact secrets in diffs
Config migrations now transform the authored overrides of a withOverrides({...}) file and re-emit the wrapper (instead of flattening it to JSON and inlining defaults); plain object exports are unchanged and the baseline is skipped. Secrets are redacted in read-only/dry-run diffs, undefined-valued keys are preserved, multiple config migrations compose on one file, mutate() gets a context.merged arg, and a non-writable conf dir auto-degrades to report-only.
Copy file name to clipboardExpand all lines: docs/migrations.md
+58-19Lines changed: 58 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ The migrations module provides a convention-based system for running data and co
8
8
2. Each file is compared against the `migrations` collection to determine what has already run
9
9
3. Pending migrations are sorted by version and executed in order
10
10
4. Completed migrations are recorded so they never run twice
11
-
5.If any config file migrations ran, the app throws a fatal error to force a restart
11
+
5.Any config file changes are written to disk, but only take effect on the next restart
12
12
13
13
## File naming
14
14
@@ -31,7 +31,7 @@ Choose versions that correspond to the module release that requires the migratio
31
31
32
32
## Execution order
33
33
34
-
Pending migrations are sorted globally by semver version, then alphabetically by module name, then by type (`data` before `conf`). This ensures data migrations take effect in the current boot before config migrations trigger a restart.
34
+
Pending migrations are sorted globally by semver version, then alphabetically by module name, then by type (`data` before `conf`). This ensures data migrations take effect in the current boot before config migrations, whose file changes only take effect on the next restart.
Empty module sections are automatically cleaned up after all operations run.
219
+
On a [`withOverrides` install](#the-withoverrides-round-trip)`config` is the file's authored **overrides** — a module section that only exists in the baseline defaults will be absent. Write null-safely, and read a baseline value via `context.merged` (a snapshot of the fully-merged config at boot):
A `mutate` that throws (e.g. assumes a section that isn't in the overrides) is reported as a per-file warning; that file is skipped and the migration is left pending so it re-runs once the mutate is made null-safe. Empty module sections are automatically cleaned up after all operations run.
220
230
221
231
### Chaining
222
232
@@ -238,24 +248,38 @@ export default function (migration) {
238
248
239
249
### How config files are processed
240
250
241
-
For each pending config migration, the framework:
251
+
On the first config migration of a boot, every `conf/*.config.js` file is imported once into a shared working-copy cache, keyed by file. Each pending config migration then runs against that cache, so multiple migrations **compose** on the same file instead of each overwriting the last. For each file, the framework:
242
252
243
-
1. Finds all `conf/*.config.js` files in the application root directory
244
-
2. Dynamically imports each file to get the config object
245
-
3. Serializes the config before running the migration
246
-
4. Runs all registered operations against the config object
247
-
5. Compares the serialized output — only writes back if the config actually changed
248
-
6. In dry-run mode, logs which files would be written without persisting
253
+
1. Runs the migration's operations against the cached working copy
254
+
2. Compares the serialized output — only writes back if the config actually changed
255
+
3. Preserves `key: undefined` entries (used to unset an inherited default), which plain JSON would drop
256
+
4. In dry-run mode, logs which files would be written without persisting
249
257
250
-
### Restart behaviour
258
+
### The withOverrides round-trip
251
259
252
-
Config files are loaded at startup, so changes won't take effect until the process restarts. After all migrations complete, if any config file migrations ran successfully (non-dry-run), the module throws a fatal error:
260
+
An instance may keep its `conf/*.config.js` as a plain object (`export default { ... }`) or layer its settings over a shared baseline:
Config file(s) modified by N migration(s). Restart required to load updated configuration.
256
-
```
257
271
258
-
Process managers (pm2, systemd, Docker) will automatically restart the app, which then picks up the updated config and boots normally. The config migrations are already recorded as complete and will not re-run.
272
+
Config migrations round-trip both styles. Each imported config file is classified by two non-enumerable markers a `withOverrides` helper attaches (`Symbol.for('adapt-authoring:configOverrides')` on the merged result, `Symbol.for('adapt-authoring:configDefaults')` on the baseline):
273
+
274
+
- **plain** — a plain object export. Operations run against the whole object; re-emitted as `exportdefault { ... }` (the original behaviour).
275
+
- **overrides** — a `withOverrides({...})` file. Operations run against the authored **overrides only** (not the merged defaults); re-emitted as `exportdefaultwithOverrides({ ... })` so the wrapper and inherited defaults are preserved rather than inlined. A key that lives only in the baseline is a no-op here — the baseline carries that change.
276
+
- **defaults** — the baseline file itself. **Skipped**: it is maintained by hand in the same release that ships the migration.
277
+
278
+
A plain instance needs no markers and is unaffected. To adopt the pattern, have your baseline's `withOverrides` attach the markers (non-enumerable, so the config loader and `JSON.stringify` never see them; global `Symbol.for` so this module reads them without importing your config).
279
+
280
+
### Restart behaviour
281
+
282
+
Config files are read once at startup — before migrations run — so changes a migration writes to disk **take effect on the next restart**, not the current boot. Nothing is thrown to force this; run under a process manager (pm2, systemd, Docker) if you want an automatic restart after config changes. The migrations are recorded as complete and will not re-run.
259
283
260
284
### Read-only config
261
285
@@ -271,7 +295,22 @@ Some deployments keep `conf/*.config.js` under version control or config managem
271
295
272
296
When enabled, each config migration that would change a file logs a `[READ-ONLY CONFIG]` warning naming the file and the module@version, followed by the same key-level diff shown in dry-run mode, then skips the write — you apply the change by hand.
273
297
274
-
Because no file is written, no restart is forced. The migration is also **not** recorded as complete, so it re-runs (and re-warns) on every boot until you make the change manually; once the config matches, the computed diff is empty and the warning stops. This affects config migrations only — data migrations still run and are recorded as normal.
298
+
The migration is **not** recorded as complete, so it re-runs (and re-warns) on every boot until you make the change manually; once the config matches, the computed diff is empty and the warning stops. This affects config migrations only — data migrations still run and are recorded as normal.
299
+
300
+
Report-only mode is also entered **automatically** when the `conf` directory isn't writable — the module checks write access up front, and defensively falls back if a write is denied (`EACCES`/`EROFS`/`EPERM`). So on a deployment with a read-only conf dir you get the same report-and-diff behaviour with no configuration; the warning notes `(conf dir is not writable)`. Set `readOnlyConfig: true` only to force report-only where the conf dir *is* writable but you still don't want the app to touch it (e.g. version-controlled config).
301
+
302
+
#### Secret redaction
303
+
304
+
The key-level diff (in read-only and dry-run modes) never prints secret values. A leaf key that looks sensitive — matching `secret`, `password`, `token`, `apiKey`, `credential`, `connectionUri`, `privateKey`, `passphrase` and similar — is shown as `[redacted]` (`~ …auth.tokenSecret: [redacted] -> [redacted]`), and secrets nested inside a non-sensitive key's object value are masked in place. Add extra patterns with `redactKeys` (regex sources, additive to the built-ins — they can never disable them):
305
+
306
+
```javascript
307
+
{
308
+
'adapt-authoring-migrations': {
309
+
readOnlyConfig: true,
310
+
redactKeys: ['licenceKey', 'internalToken']
311
+
}
312
+
}
313
+
```
275
314
276
315
### Cross-module config moves
277
316
@@ -292,9 +331,9 @@ Behaviour differs by deployment:
292
331
- **Replica set (transactions available)** — each data migration runs for real inside a transaction that is then aborted. Reads and mutations execute against live data, so `where()` matching and `check()` validation are exercised exactly as in a real run, but nothing is committed.
293
332
- **Standalone mongod (no transactions)** — data migrations run through a read-only proxy. Reads execute normally, but write methods (`insertOne`, `updateMany`, `drop`, `createIndex`, `renameCollection`, etc.) are intercepted and logged instead of executed, e.g. `[DRY RUN] courses.updateMany({...})`.
294
333
295
-
Config file migrations compute the change and log a key-level diff (`+` added, `-` removed, `~` changed) followed by `would write <file>`, but leave the files untouched.
334
+
Config file migrations compute the change and log a key-level diff (`+` added, `-` removed, `~` changed, [secrets redacted](#secret-redaction)) followed by `would write <file>`, but leave the files untouched.
296
335
297
-
A dry run never records anything in the `migrations` collection and never triggers the [restart](#restart-behaviour) that config migrations normally force. Completion state is also ignored, so a dry run reports **every** discovered migration as pending — including ones already applied — giving you the full set that would run against a fresh database.
336
+
A dry run never records anything in the `migrations` collection. Completion state is also ignored, so a dry run reports **every** discovered migration as pending — including ones already applied — giving you the full set that would run against a fresh database.
0 commit comments