@bongos/core 1.21.115 → 1.21.116
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 +20 -20
- package/docs/copy-inventory.md +50 -50
- package/docs/copy-registry.json +56 -56
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +1 -1
- package/modules/hall-ui/public/deploy.js +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/upgrade-migrate.js +62 -2
- package/scripts/gds/upgrade-outcome.js +4 -0
- package/scripts/gds/upgrade.js +6 -6
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +15 -0
- package/tests/instance_db_migrated_on_code_move.mjs +2 -2
- package/tests/upgrade_migrate.mjs +81 -0
package/scripts/gds/upgrade.js
CHANGED
|
@@ -33,7 +33,7 @@ const { parseSemver, compareSemver, isStable, listAvailableVersions } = require(
|
|
|
33
33
|
const { checkArtistGate } = require('./upgrade-artist-gate');
|
|
34
34
|
// The database side of a move: which command migrates, as which account, and whether that
|
|
35
35
|
// account can reach the database (tasks 1004274, 1002712, 1004512).
|
|
36
|
-
const { preflightDbIdentity, asAccount, ACCOUNT_RE } = require('./upgrade-migrate');
|
|
36
|
+
const { preflightDbIdentity, asAccount, ACCOUNT_RE, teeSpawn, migrateResult, migrateError } = require('./upgrade-migrate');
|
|
37
37
|
|
|
38
38
|
const CORE_PKG = '@bongos/core';
|
|
39
39
|
const ARTIFACT = 'bongos-core';
|
|
@@ -519,13 +519,13 @@ function npmInstall(instanceDir, run = spawnSync) {
|
|
|
519
519
|
return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
function runMigrate(instanceDir, run = spawnSync, account = null) {
|
|
522
|
+
async function runMigrate(instanceDir, run = spawnSync, account = null) {
|
|
523
523
|
const { cmd, args, env } = require('./upgrade-migrate').migratePlan(instanceDir); // its own migrate script, else the core's migrate.sh (task 1004274)
|
|
524
524
|
// `account`: the project's own unix+Postgres identity when the move itself is running as
|
|
525
525
|
// the account that owns the CHECKOUT instead (--migrate-as; upgrade-migrate.js, task 1004512).
|
|
526
526
|
const q = asAccount(account, cmd, args, env ? { INIT_CWD: instanceDir } : {});
|
|
527
|
-
|
|
528
|
-
return
|
|
527
|
+
// TEED, not inherited: still streams live, and a failure names its migration + psql's ERROR line (upgrade-migrate.js, task 1004734)
|
|
528
|
+
return migrateResult(await (run === spawnSync ? teeSpawn : run)(q.cmd, q.args, { cwd: instanceDir, ...(env ? { env } : {}) }));
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
// WHICH database will migrate target? (task 1002755)
|
|
@@ -1224,9 +1224,9 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
1224
1224
|
|
|
1225
1225
|
// 4. migrate (additive core_* migrations).
|
|
1226
1226
|
if (!opts.skipMigrate) {
|
|
1227
|
-
const mig = runMigrate(instanceDir, run, opts.migrateAs || null);
|
|
1227
|
+
const mig = await runMigrate(instanceDir, run, opts.migrateAs || null);
|
|
1228
1228
|
if (!mig.ok) {
|
|
1229
|
-
const error =
|
|
1229
|
+
const error = migrateError(mig); // names the failing migration + its ERROR line (task 1004734)
|
|
1230
1230
|
if (rollbackOnFailure && snapshot) {
|
|
1231
1231
|
const rb = await doRollback('migrate failed');
|
|
1232
1232
|
return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb };
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.116'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -1551,6 +1551,21 @@ await t('a real rollback says rolled back', async () => {
|
|
|
1551
1551
|
assert.match(r.error, /failed and was rolled back/);
|
|
1552
1552
|
});
|
|
1553
1553
|
|
|
1554
|
+
await t('a failed migrate reaches the stored failure with its migration and psql\'s ERROR line (task 1004734)', async () => {
|
|
1555
|
+
// upgrade.js's REAL migrate result → its real error sentence → its real outcome line, in the
|
|
1556
|
+
// frame its main() writes to stderr (pinned by "upgrade.js ends a failed run…" below).
|
|
1557
|
+
const run = () => ({ status: 3, stdout: 'apply core_998_ok\napply core_999_x\n',
|
|
1558
|
+
stderr: 'psql:migrations/core_999_x.sql:7: ERROR: check constraint "tasks_kind_check" of relation "tasks" is violated by some row\n' });
|
|
1559
|
+
const mig = await U.runMigrate(fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'mig-')), run);
|
|
1560
|
+
const res = { ok: false, error: require('../scripts/gds/upgrade-migrate.js').migrateError(mig), rolledBack: true, rollback: {} };
|
|
1561
|
+
const stderr = `\n✖ ${res.error}\n${require('../scripts/gds/upgrade-outcome.js').outcomeLine(res)}`;
|
|
1562
|
+
const r = await R.coreUpgradeInstance(inst(), upgradeThrows(stderr)(makeDeps()), intent());
|
|
1563
|
+
assert.deepEqual(r.failure, { class: 'unknown', reason: 'migrate_check_violation' });
|
|
1564
|
+
assert.match(r.error, /failed and was rolled back/);
|
|
1565
|
+
assert.match(r.error, /core_999_x/, 'the stored sentence names the migration');
|
|
1566
|
+
assert.match(r.error, /ERROR: +check constraint "tasks_kind_check" of relation "tasks" is violated by some row/, 'and psql\'s own error');
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1554
1569
|
await t('a FAILED rollback is its own outcome, and terminal — never retried unattended', async () => {
|
|
1555
1570
|
const d = upgradeThrows('\n✖ health check failed after restart\nBONGOS_UPGRADE_OUTCOME {"stage":"rollback_failed","reason":"rollback_failed"}\n')(makeDeps());
|
|
1556
1571
|
const r = await R.coreUpgradeInstance(inst(), d, intent());
|
|
@@ -32,7 +32,7 @@ const tenant = { slug: 'charter', db_name: 'charter_db', hosting_shape: 'co-tena
|
|
|
32
32
|
|
|
33
33
|
test('bongos upgrade migrates before it restarts, and only --skip-migrate opts out', () => {
|
|
34
34
|
const src = fs.readFileSync(path.join(ROOT, 'scripts/gds/upgrade.js'), 'utf8');
|
|
35
|
-
const migrate = src.indexOf('const mig = runMigrate(instanceDir, run, opts.migrateAs || null);');
|
|
35
|
+
const migrate = src.indexOf('const mig = await runMigrate(instanceDir, run, opts.migrateAs || null);'); // async since task 1004734 (teed output)
|
|
36
36
|
const restart = src.lastIndexOf('restartService({ service: opts.service, restartCmd: opts.restartCmd }');
|
|
37
37
|
assert.ok(migrate > 0, 'the upgrade flow runs migrate');
|
|
38
38
|
assert.ok(restart > migrate, 'the restart that makes new code live comes AFTER migrate');
|
|
@@ -47,7 +47,7 @@ test('bongos upgrade migrates before it restarts, and only --skip-migrate opts o
|
|
|
47
47
|
// provisioned since task 1003369, and the wrong one cannot connect at all: the project's
|
|
48
48
|
// database is owned by its own Postgres login role with CONNECT revoked from PUBLIC.
|
|
49
49
|
// Asserting the CALL SITE, not just the helper — the helper was never the gap.
|
|
50
|
-
assert.match(src, /const mig = runMigrate\(instanceDir, run, opts\.migrateAs \|\| null\);/,
|
|
50
|
+
assert.match(src, /const mig = await runMigrate\(instanceDir, run, opts\.migrateAs \|\| null\);/,
|
|
51
51
|
'the migrate step is handed --migrate-as, not left on whoever the move happens to be');
|
|
52
52
|
assert.equal(`bash ${CORE_MIGRATE_SCRIPT}`, INSTANCE_MIGRATE_SCRIPT, 'migrate is the instance scaffold script, i.e. the core migrate.sh');
|
|
53
53
|
});
|
|
@@ -23,6 +23,9 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, realpathSync, syml
|
|
|
23
23
|
import { tmpdir } from 'node:os';
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { EventEmitter } from 'node:events';
|
|
27
|
+
// ESM named imports: knip credits a CJS export only when a test imports it this way.
|
|
28
|
+
import { migrateError, migrateFailureDetail, teeSpawn } from '../scripts/gds/upgrade-migrate.js';
|
|
26
29
|
|
|
27
30
|
const require = createRequire(import.meta.url);
|
|
28
31
|
const { migratePlan, CORE_MIGRATE_SCRIPT } = require('../scripts/gds/upgrade-migrate.js');
|
|
@@ -193,3 +196,81 @@ test('the real fallback command resolves the project as its instance root, despi
|
|
|
193
196
|
const blind = rootsOf(execFileSync('bash', [CORE_MIGRATE_SCRIPT, '--print-roots'], { cwd: dir, env: base, encoding: 'utf8' }));
|
|
194
197
|
assert.notEqual(blind.INSTANCE_ROOT, dir, 'without INIT_CWD pinned, a stray value decides which project is migrated');
|
|
195
198
|
});
|
|
199
|
+
|
|
200
|
+
// ── a failed migrate names its migration and psql's ERROR line (task 1004734) ──────────
|
|
201
|
+
// Seen 2026-10-14: toast's move 1.21.20 → 1.21.115 rolled back and the hub kept only
|
|
202
|
+
// "migrate failed (exit 3)". Which file and which error were in the box's journal alone.
|
|
203
|
+
const FAKE_DSN = ['postgres://app', 'Sup3rS3cretPassw0rd@db.internal/prod'].join(':');
|
|
204
|
+
const MIGRATE_OUT = 'skip core_001_base (already applied)\napply core_998_ok\napply core_999_x\n';
|
|
205
|
+
const PSQL_ERR = `psql:node_modules/@bongos/core/migrations/core_999_x.sql:7: ERROR: check constraint "tasks_kind_check" of relation "tasks" is violated by some row ${FAKE_DSN}\n`;
|
|
206
|
+
|
|
207
|
+
function failingMigrateRun(calls, opts = {}) {
|
|
208
|
+
const base = recordingRun(calls, opts);
|
|
209
|
+
return (cmd, args = [], o = {}) => {
|
|
210
|
+
if ((cmd === 'bash' && args[0] === CORE_MIGRATE_SCRIPT) || (cmd === 'npm' && args[0] === 'run' && args[1] === 'migrate')) {
|
|
211
|
+
calls.push({ line: `${cmd} ${args.join(' ')}`.trim(), opts: o });
|
|
212
|
+
return { status: 3, stdout: MIGRATE_OUT, stderr: PSQL_ERR };
|
|
213
|
+
}
|
|
214
|
+
return base(cmd, args, o);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
test('runMigrate: a failing migration comes back with its file name and psql\'s ERROR line', async () => {
|
|
219
|
+
const mig = await u.runMigrate(adoptedProject(), failingMigrateRun([]));
|
|
220
|
+
assert.equal(mig.ok, false);
|
|
221
|
+
assert.equal(mig.code, 3);
|
|
222
|
+
const error = migrateError(mig);
|
|
223
|
+
assert.match(error, /^migrate failed \(exit 3\)/, 'the prefix upgrade-outcome.js keys on is unchanged');
|
|
224
|
+
assert.match(error, /core_999_x/, 'names the migration that was being applied when it stopped');
|
|
225
|
+
assert.doesNotMatch(error, /core_998_ok/, 'and not an earlier one that applied cleanly');
|
|
226
|
+
assert.match(error, /ERROR: +check constraint "tasks_kind_check" of relation "tasks" is violated by some row/);
|
|
227
|
+
assert.doesNotMatch(error, /Sup3rS3cretPassw0rd/, 'a credential in the error never reaches the hub');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('migrateFailureDetail scrubs secrets and caps the length', () => {
|
|
231
|
+
const d = migrateFailureDetail(`apply core_999_x\nERROR: bad value ${FAKE_DSN} ${'x'.repeat(5000)}\n`);
|
|
232
|
+
assert.doesNotMatch(d, /Sup3rS3cretPassw0rd/);
|
|
233
|
+
assert.ok(d.length <= 400, `capped (${d.length})`);
|
|
234
|
+
assert.match(d, /core_999_x/);
|
|
235
|
+
assert.equal(migrateFailureDetail('npm error code 1\n'), '', 'nothing recognisable adds nothing');
|
|
236
|
+
assert.match(migrateFailureDetail('psql:/x/migrations/core_5_y.sql:3: ERROR: boom\n'), /core_5_y.*ERROR: +boom/, 'psql\'s own file prefix stands in for a missing apply line');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('a full upgrade whose migrate fails reports the migration, the error, and a CHECK-violation class', async () => {
|
|
240
|
+
const dir = adoptedProject({ installed: '1.19.312' });
|
|
241
|
+
const res = await u.runUpgrade(
|
|
242
|
+
{ to: '1.19.1029', registry: true, instance: dir, service: 'mercury', healthCheck: false },
|
|
243
|
+
{
|
|
244
|
+
log: () => {}, err: () => {},
|
|
245
|
+
run: failingMigrateRun([], { dir, installTo: '1.19.1029' }),
|
|
246
|
+
materialize: () => ({ skills: 0 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
|
|
247
|
+
databaseUrl: '', pgDatabase: '',
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
assert.equal(res.ok, false);
|
|
251
|
+
assert.match(res.error, /^migrate failed \(exit 3\) at core_999_x: ERROR: +check constraint "tasks_kind_check"/);
|
|
252
|
+
const outcome = require('../scripts/gds/upgrade-outcome.js');
|
|
253
|
+
assert.equal(outcome.upgradeReason(res), 'migrate_check_violation');
|
|
254
|
+
assert.equal(outcome.classifyFailure('migrate_check_violation').class, 'unknown', 'existing rows break the new rule: a person decides, never an unattended retry');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('teeSpawn streams the child\'s output live AND keeps it for the failure report', async () => {
|
|
258
|
+
const pipe = () => Object.assign(new EventEmitter(), { setEncoding() {} });
|
|
259
|
+
let child;
|
|
260
|
+
const spawnImpl = (cmd, args, opts) => {
|
|
261
|
+
child = Object.assign(new EventEmitter(), { stdout: pipe(), stderr: pipe(), opts });
|
|
262
|
+
return child;
|
|
263
|
+
};
|
|
264
|
+
const out = [], err = [];
|
|
265
|
+
const p = teeSpawn('bash', ['m.sh'], { cwd: '/x' }, { spawnImpl, out: { write: (c) => out.push(c) }, err: { write: (c) => err.push(c) } });
|
|
266
|
+
child.stdout.emit('data', 'apply core_999_x\n');
|
|
267
|
+
assert.deepEqual(out, ['apply core_999_x\n'], 'written through as it arrives, not after the child exits');
|
|
268
|
+
child.stderr.emit('data', 'ERROR: boom\n');
|
|
269
|
+
assert.deepEqual(err, ['ERROR: boom\n']);
|
|
270
|
+
child.emit('close', 3);
|
|
271
|
+
const r = await p;
|
|
272
|
+
assert.equal(r.status, 3);
|
|
273
|
+
assert.match(r.output, /apply {2}core_999_x[\s\S]*ERROR: {2}boom/);
|
|
274
|
+
assert.deepEqual(child.opts.stdio, ['inherit', 'pipe', 'pipe']);
|
|
275
|
+
assert.equal(child.opts.cwd, '/x');
|
|
276
|
+
});
|