@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.
- package/.bongos-core.json +30 -15
- package/docs/design/mocks/projects-hub-mock.html +991 -0
- package/docs/design/mocks/projects-hub-variants.html +146 -0
- package/docs/design/projects-hub-direction.md +347 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/upgrade.js +47 -4
- package/scripts/migrate.sh +60 -3
- package/src/module-api.js +5 -1
- package/tests/migrate_root_resolution.mjs +100 -2
- package/tests/upgrade.mjs +58 -0
package/scripts/migrate.sh
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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);
|