@bongos/core 1.19.91 → 1.19.93

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.
@@ -32,7 +32,13 @@ INVOKE_CWD="${INIT_CWD:-$PWD}"
32
32
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
33
33
  DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
34
34
  CORE_ROOT="$(cd "$DEFAULT_ROOT" && node -e "process.stdout.write(require('./src/instance-config').resolveCoreRoot())" 2>/dev/null || echo "$DEFAULT_ROOT")"
35
- INSTANCE_ROOT="$(cd "$INVOKE_CWD" && node -e "process.stdout.write(require('$DEFAULT_ROOT/src/instance-config').resolveInstanceRootExplicit() || '')" 2>/dev/null || true)"
35
+ # The module path rides in as argv rather than being interpolated into the JS source: a
36
+ # Windows-shaped DEFAULT_ROOT contains backslashes, which are ESCAPE SEQUENCES inside a JS
37
+ # string literal ("\U", "\r", "\D"), so interpolating corrupts the path, the require throws,
38
+ # and this silently falls back to DEFAULT_ROOT — precisely the F6 shape described above, just
39
+ # triggered by the path separator instead of the cwd. Fixed alongside task 1002755, which hit
40
+ # the identical bug one resolution further down.
41
+ INSTANCE_ROOT="$(cd "$INVOKE_CWD" && node -e "process.stdout.write(require(process.argv[1]).resolveInstanceRootExplicit() || '')" "$DEFAULT_ROOT/src/instance-config" 2>/dev/null || true)"
36
42
  INSTANCE_ROOT="${INSTANCE_ROOT:-$DEFAULT_ROOT}"
37
43
  # Run from the instance root so instance-relative paths resolve (the post-deploy art check below)
38
44
  # AND so the enabledModules() read finds the instance's config/modules.json (resolveInstanceRoot
@@ -49,17 +55,68 @@ if [ "${1:-}" = "--print-roots" ]; then
49
55
  exit 0
50
56
  fi
51
57
 
52
- DB="${PGDATABASE:-amazonprimea}"
53
-
54
58
  # Test/ops seam (added with the 1.18.19 space-in-path fix): --print-migrations walks the REAL core-migration loop but reports
55
59
  # each path instead of applying it, and touches NO DB. tests/migrate_root_resolution.mjs uses it to
56
60
  # assert that a checkout whose path contains a space still yields whole, readable paths. Reporting
57
61
  # through the real loop (rather than a copy of it in the test) is the point: if the loop ever reverts
58
62
  # to an unquoted `for f in $MIGRATIONS`, this seam fragments the paths and the test fails. Also handy
59
63
  # in the field — `npm run migrate -- --print-migrations` shows exactly what migrate would apply.
64
+ #
65
+ # Resolved BEFORE the database name below, because a dry run touches no DB (every psql call is
66
+ # guarded by DRY_RUN) and so must not require one.
60
67
  DRY_RUN=0
61
68
  if [ "${1:-}" = "--print-migrations" ]; then DRY_RUN=1; fi
62
69
 
70
+ # --- Which database? (task 1002755) ---------------------------------------
71
+ # SAME precedence as src/bongos/pool.js: an explicit PGDATABASE wins, else the instance's own
72
+ # branding pack names it (config/branding.json db.database, else the neutral starter). Resolved by
73
+ # CALLING pool.instanceDbName() rather than re-reading the config here, so the migrator and the
74
+ # running server can never disagree about which database this instance is.
75
+ #
76
+ # There is deliberately NO literal fallback. This script ships inside the core to EVERY instance,
77
+ # so a baked-in default is one host's database name pointed at everyone else's box: on a co-hosted
78
+ # machine it silently targets a stranger's data, and everywhere else it dies with a confusing
79
+ # "database ... does not exist" long after the caller has moved its version pin. pool.js already
80
+ # made exactly this call for the server ("NOT hard-defaulting to a host's DB name", pool.js:10-16);
81
+ # this is the migrator catching up, not a new policy.
82
+ # The module path rides in as argv, NOT interpolated into the JS source: a Windows-shaped
83
+ # CORE_ROOT contains backslashes, and inside a JS string literal those are escape sequences
84
+ # ("\U", "\r"), so interpolating silently corrupts the path and this resolution degrades to
85
+ # empty. argv values are never parsed as JS.
86
+ DB="${PGDATABASE:-}"
87
+ DB_RESOLVER_ERR=""
88
+ if [ -z "$DB" ]; then
89
+ # stderr is CAPTURED, not discarded: "the pack names no database" and "the resolver could not
90
+ # run at all" are different problems with different fixes, and collapsing both into an empty
91
+ # string sends the operator to edit a config file when the real answer is `npm install`.
92
+ # (pool.js requires pg, so an instance with no node_modules fails here rather than resolving.)
93
+ if DB_RESOLVED="$(node -e "process.stdout.write(require(process.argv[1]).instanceDbName() || '')" "$CORE_ROOT/src/bongos/pool" 2>&1)"; then
94
+ DB="$DB_RESOLVED"
95
+ else
96
+ DB_RESOLVER_ERR="$DB_RESOLVED"
97
+ fi
98
+ fi
99
+ if [ -z "$DB" ] && [ "$DRY_RUN" = 0 ]; then
100
+ echo "migrate: cannot determine which database to migrate." >&2
101
+ if [ -n "$DB_RESOLVER_ERR" ]; then
102
+ echo " the db-name resolver did not run: $(printf '%s' "$DB_RESOLVER_ERR" | head -n 1)" >&2
103
+ echo " if the core's dependencies are not installed yet, run: npm install" >&2
104
+ else
105
+ echo " Set PGDATABASE, or give this instance a db.database in config/branding.json." >&2
106
+ fi
107
+ echo " Refusing to guess: a baked-in default would target another instance's database." >&2
108
+ exit 2
109
+ fi
110
+
111
+ # Test/ops seam: --print-db reports the database this run WOULD migrate, and exits before any DB
112
+ # work. `bongos upgrade` calls it to learn the name up front, so its pre-flight can prove the DB is
113
+ # reachable BEFORE the version pin moves — otherwise an unresolvable name is only discovered at
114
+ # migrate time, and the pin has to be unwound (task 1002755).
115
+ if [ "${1:-}" = "--print-db" ]; then
116
+ printf '%s\n' "$DB"
117
+ exit 0
118
+ fi
119
+
63
120
  # --- Instance-seed policy (task 1704 / ADR 0105) --------------------------
64
121
  # OTB/product scope-truth (versions, goals, done_when_criteria) lives in seed
65
122
  # migrations under migrations/instance/ (plus a few guarded blocks embedded in the
package/src/module-api.js CHANGED
@@ -609,8 +609,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
609
609
  // landed since 1.19.89 with no explicit bump. run 31717551308. (task 1002620)
610
610
  // 1.19.91 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
611
611
  // landed since 1.19.90 with no explicit bump. run 31722841411. (task 1002620)
612
+ // 1.19.92 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
613
+ // landed since 1.19.91 with no explicit bump. run 31727411036. (task 1002620)
614
+ // 1.19.93 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
615
+ // landed since 1.19.92 with no explicit bump. run 31735192921. (task 1002620)
612
616
  // ---------------------------------------------------------------------------
613
- const CORE_VERSION = '1.19.91'; // CI auto-patch carrier (ADR 0161); see changelog above
617
+ const CORE_VERSION = '1.19.93'; // CI auto-patch carrier (ADR 0161); see changelog above
614
618
 
615
619
  // A namespaced logger so a module's log lines are attributable + consistent.
616
620
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -18,7 +18,7 @@
18
18
  import { strict as assert } from 'node:assert';
19
19
  import { execFileSync } from 'node:child_process';
20
20
  import {
21
- mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, symlinkSync, realpathSync,
21
+ mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, symlinkSync, copyFileSync, realpathSync,
22
22
  } from 'node:fs';
23
23
  import { tmpdir } from 'node:os';
24
24
  import { fileURLToPath } from 'node:url';
@@ -116,7 +116,15 @@ function makeCore({ spaced, files = ['001_a.sql', '002_b.sql', '006_pms_v2.sql',
116
116
  mkdirSync(path.join(dir, 'scripts'));
117
117
  mkdirSync(path.join(dir, 'migrations'));
118
118
  mkdirSync(path.join(dir, 'src'));
119
- symlinkSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
119
+ // Symlink so the fixture can never drift from the real script. Windows refuses symlinks without
120
+ // elevation (EPERM), so fall back to a COPY taken fresh from MIGRATE on each run — equally
121
+ // drift-proof, and it is what lets these cases run on a Windows checkout at all.
122
+ try {
123
+ symlinkSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
124
+ } catch (e) {
125
+ if (e.code !== 'EPERM' && e.code !== 'EACCES') throw e;
126
+ copyFileSync(MIGRATE, path.join(dir, 'scripts', 'migrate.sh'));
127
+ }
120
128
  // Pin both roots to the fixture explicitly, so the test does not lean on migrate.sh's
121
129
  // node-unavailable fallback to decide what CORE_ROOT is.
122
130
  writeFileSync(path.join(dir, 'src', 'instance-config.js'), [
@@ -216,5 +224,95 @@ t('the pre-fix unquoted loop now fails loudly instead of silently skipping', ()
216
224
  assert.equal(r.applied.length, 0, 'the unquoted loop applies nothing (that was the bug)');
217
225
  });
218
226
 
227
+ // ==========================================================================
228
+ // WHICH DATABASE does migrate target? (task 1002755)
229
+ //
230
+ // scripts/migrate.sh:52 used to read `DB="${PGDATABASE:-amazonprimea}"` — one host instance's
231
+ // database name, baked into a script the core ships to EVERY instance. With no PGDATABASE in the
232
+ // environment, a co-hosted box silently aimed at a stranger's database and everywhere else died
233
+ // with a confusing "database ... does not exist" — after the caller had already moved its version
234
+ // pin. ADR 0149:17 had already named the divergence: the server pool resolves from the branding
235
+ // pack while migrate.sh defaulted to a literal, and "they only agree if PGDATABASE is set for both."
236
+ //
237
+ // These drive the `--print-db` seam, which resolves the name and exits before any psql — the
238
+ // --print-roots pattern. The fixture carries a REAL config/branding.json so the assertions
239
+ // exercise the actual resolver (pool.instanceDbName) rather than a stub of it.
240
+
241
+ // A core fixture that is ALSO an instance: adds the branding pack instanceDbName() reads.
242
+ // db.database === null models the neutral starter — an instance that has not named its database.
243
+ function makeInstance({ database } = {}) {
244
+ const dir = makeCore({ spaced: false });
245
+ mkdirSync(path.join(dir, 'config'), { recursive: true });
246
+ writeFileSync(
247
+ path.join(dir, 'config', 'branding.json'),
248
+ `${JSON.stringify({ db: { database: database ?? null } }, null, 2)}\n`,
249
+ );
250
+ // migrate.sh resolves the name through <CORE_ROOT>/src/bongos/pool. Re-export the REAL module
251
+ // so these cases exercise the actual resolver rather than a restatement of it — a stub here
252
+ // would pass happily while pool.js drifted. The real file resolves its own `pg` from the repo's
253
+ // node_modules (module resolution walks up from ITS path, not the fixture's), and its
254
+ // resolveInstanceRoot() falls back to cwd — which migrate.sh has already set to this fixture.
255
+ mkdirSync(path.join(dir, 'src', 'bongos'), { recursive: true });
256
+ writeFileSync(
257
+ path.join(dir, 'src', 'bongos', 'pool.js'),
258
+ `module.exports = require(${JSON.stringify(path.join(REPO_ROOT, 'src', 'bongos', 'pool.js'))});\n`,
259
+ );
260
+ return dir;
261
+ }
262
+
263
+ function printDb(coreDir, extraEnv = {}) {
264
+ const env = { ...process.env, ...extraEnv };
265
+ for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
266
+ if (!('PGDATABASE' in extraEnv)) delete env.PGDATABASE;
267
+ try {
268
+ const stdout = execFileSync('bash', [path.join(coreDir, 'scripts', 'migrate.sh'), '--print-db'],
269
+ { cwd: coreDir, env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
270
+ return { status: 0, stdout: stdout.trim(), stderr: '' };
271
+ } catch (e) {
272
+ return { status: e.status ?? 1, stdout: String(e.stdout || '').trim(), stderr: String(e.stderr || '') };
273
+ }
274
+ }
275
+
276
+ t('--print-db reports the database named by the instance\'s own branding pack', () => {
277
+ const r = printDb(makeInstance({ database: 'acme_instance_db' }));
278
+ assert.equal(r.status, 0, r.stderr);
279
+ assert.equal(r.stdout, 'acme_instance_db');
280
+ });
281
+
282
+ t('an explicit PGDATABASE wins over the pack — same precedence as pool.js', () => {
283
+ const r = printDb(makeInstance({ database: 'from_the_pack' }), { PGDATABASE: 'from_the_env' });
284
+ assert.equal(r.status, 0, r.stderr);
285
+ assert.equal(r.stdout, 'from_the_env');
286
+ });
287
+
288
+ t('an instance that names no database is REFUSED, never defaulted', () => {
289
+ const r = printDb(makeInstance({ database: null }));
290
+ assert.equal(r.status, 2, 'must exit 2 — the seam refuses rather than printing an empty name');
291
+ assert.doesNotMatch(r.stdout, /\S/, 'nothing is printed as the database name');
292
+ assert.match(r.stderr, /cannot determine which database/i);
293
+ assert.match(r.stderr, /Refusing to guess/i);
294
+ // the regression itself: it must not reach for another instance's database
295
+ assert.doesNotMatch(`${r.stdout}${r.stderr}`, /amazonprimea/i);
296
+ });
297
+
298
+ t('--print-migrations stays DB-free — a dry run must not require a database name', () => {
299
+ // the seam walks the real loop and touches no psql, so an instance with no db.database
300
+ // must still be able to list what would be applied
301
+ const core = makeInstance({ database: null });
302
+ const r = printMigrations(core);
303
+ assert.equal(r.status, 0, `a dry run must not need a database: ${r.stderr}`);
304
+ assert.equal(r.listed, 4);
305
+ });
306
+
307
+ t('migrate.sh hardcodes no instance database name', () => {
308
+ // the static lock on the original defect — a literal default reintroduced anywhere in the
309
+ // script points every other instance at a database that is not theirs
310
+ const src = readFileSync(MIGRATE, 'utf8');
311
+ const code = src.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join('\n');
312
+ assert.doesNotMatch(code, /PGDATABASE:-\s*[A-Za-z]/,
313
+ 'PGDATABASE must have no literal fallback — resolve from the branding pack instead');
314
+ assert.doesNotMatch(code, /amazonprimea/i, 'no instance database name in executable lines');
315
+ });
316
+
219
317
  console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
220
318
  process.exit(failed ? 1 : 0);
package/tests/upgrade.mjs CHANGED
@@ -1033,6 +1033,64 @@ t('runUpgrade: a REFUSED bump still reports the non-blocking module findings', a
1033
1033
  assert.match(text, /author unreachable/);
1034
1034
  });
1035
1035
 
1036
+ // ── which database will migrate target? (task 1002755) ────────────────────
1037
+ // The pre-flight probe is only meaningful if it tests the SAME database migrate will use. It used
1038
+ // to be handed process.env.PGDATABASE — undefined on a normal box — so psql fell through to a
1039
+ // database named after the OS user, failed for an unrelated reason, and soft-skipped; migrate then
1040
+ // picked its own name and died on a database that did not exist, AFTER the pin had moved.
1041
+ // The three-way result is the point: "names no database" and "resolver could not run" are
1042
+ // different problems and only the first may block a bump.
1043
+
1044
+ t('resolveInstanceDb: an explicit PGDATABASE wins without spawning anything', () => {
1045
+ const prev = process.env.PGDATABASE;
1046
+ process.env.PGDATABASE = 'from_the_env';
1047
+ try {
1048
+ let spawned = false;
1049
+ const r = u.resolveInstanceDb({ instanceDir: '/x' }, () => { spawned = true; return { status: 0, stdout: 'other' }; });
1050
+ assert.deepEqual(r, { db: 'from_the_env', ran: true });
1051
+ assert.equal(spawned, false, 'the env answer needs no child process');
1052
+ } finally {
1053
+ if (prev === undefined) delete process.env.PGDATABASE; else process.env.PGDATABASE = prev;
1054
+ }
1055
+ });
1056
+
1057
+ t('resolveInstanceDb: reports the name the core resolver printed', () => {
1058
+ const prev = process.env.PGDATABASE;
1059
+ delete process.env.PGDATABASE;
1060
+ try {
1061
+ const r = u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ status: 0, stdout: 'acme_db\n' }));
1062
+ assert.deepEqual(r, { db: 'acme_db', ran: true });
1063
+ } finally { if (prev !== undefined) process.env.PGDATABASE = prev; }
1064
+ });
1065
+
1066
+ t('resolveInstanceDb: ran-but-empty is FATAL, unrunnable is not', () => {
1067
+ const prev = process.env.PGDATABASE;
1068
+ delete process.env.PGDATABASE;
1069
+ try {
1070
+ // the instance genuinely names no database — knowable, and a bump must stop
1071
+ assert.deepEqual(u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ status: 0, stdout: '' })),
1072
+ { db: null, ran: true });
1073
+ // the resolver could not run — a DIFFERENT problem; must not be reported as "no database"
1074
+ assert.deepEqual(u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ status: 1, stdout: '' })),
1075
+ { db: null, ran: false });
1076
+ assert.deepEqual(u.resolveInstanceDb({ instanceDir: '/x' }, () => ({ error: new Error('ENOENT') })),
1077
+ { db: null, ran: false });
1078
+ } finally { if (prev !== undefined) process.env.PGDATABASE = prev; }
1079
+ });
1080
+
1081
+ t('runUpgrade: refuses BEFORE the pin moves when the instance names no database', async () => {
1082
+ const dir = mkdtempSync(join(tmpdir(), 'nodb-'));
1083
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '1.0.0' } }, null, 2));
1084
+ const res = await u.runUpgrade({ to: '1.0.1', instance: dir, registry: true, force: true }, {
1085
+ resolveInstanceDb: () => ({ db: null, ran: true }),
1086
+ log: () => {},
1087
+ });
1088
+ assert.equal(res.ok, false);
1089
+ assert.match(res.error, /cannot determine which database/i);
1090
+ // the pin must be untouched — that is the whole point of checking in pre-flight
1091
+ assert.match(readFileSync(join(dir, 'package.json'), 'utf8'), /"@bongos\/core": "1\.0\.0"/);
1092
+ });
1093
+
1036
1094
  await Promise.all(pending);
1037
1095
  console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
1038
1096
  process.exit(failed ? 1 : 0);