@bongos/core 1.21.116 → 1.21.118
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 +68 -33
- package/docs/adr/0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md +2 -1
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +34 -0
- package/docs/adr/0381-the-hub-moves-each-hosted-project-by-its-update-rule.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +49 -49
- package/docs/copy-registry.json +56 -56
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +1 -1
- package/docs/recipes/upgrading-the-core.md +3 -1
- package/modules/hall-ui/public/deploy.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/fitness-lib.js +35 -0
- package/scripts/gds/fitness.js +2 -2
- package/scripts/gds/provision-auto-upgrade.js +139 -0
- package/scripts/gds/provision-core-upgrade.js +26 -7
- package/scripts/gds/provision-pin-replay.js +126 -0
- package/scripts/gds/provision-repo.js +60 -0
- package/scripts/gds/provision.js +7 -1
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/upgrade-outcome.js +4 -0
- package/scripts/gds/wedge-remedy.js +13 -2
- package/src/module-api.js +1 -1
- package/tests/core_auto_upgrade.mjs +189 -0
- package/tests/db_ownership_handover.mjs +156 -0
- package/tests/fitness_installed_core.mjs +127 -0
- package/tests/pin_replay.mjs +147 -0
- package/tests/pin_write_key.mjs +26 -0
- package/tests/provision.mjs +34 -0
- package/tests/upgrade_migrate.mjs +10 -0
- package/tests/wedge_remedy.mjs +23 -1
package/tests/provision.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { join } from 'node:path';
|
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
20
|
const P = require('../scripts/gds/provision.js');
|
|
21
|
+
const PR = require('../scripts/gds/provision-repo.js'); // task 1004735: dbOwnershipSql/Cmd are not re-exported through provision.js
|
|
21
22
|
const AP = require('../scripts/gds/instance-apex.js');
|
|
22
23
|
const RP = require('../scripts/gds/provision-repo.js');
|
|
23
24
|
|
|
@@ -3370,6 +3371,39 @@ t('dbRoleGrantsCmd: runs on the INSTANCE db and grants forward too — the migra
|
|
|
3370
3371
|
assert.match(c, /GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO %I/);
|
|
3371
3372
|
});
|
|
3372
3373
|
|
|
3374
|
+
t('dbOwnershipCmd: hands OWNERSHIP of every object to the instance role, as postgres, in its own db (task 1004735)', () => {
|
|
3375
|
+
// GRANT ALL is not ownership: a later migrate run AS the instance role (--migrate-as,
|
|
3376
|
+
// task 1004512) cannot ALTER TABLE or CREATE OR REPLACE FUNCTION on an object postgres
|
|
3377
|
+
// owns ("must be owner of"), so toast's first ALTERing core move failed and rolled back.
|
|
3378
|
+
const c = PR.dbOwnershipCmd(coTenant, { privileged: true });
|
|
3379
|
+
assert.ok(c.startsWith('sudo -u postgres psql -v ON_ERROR_STOP=1 -d mercury -c '), c.slice(0, 80));
|
|
3380
|
+
for (const kind of ['TABLE', 'SEQUENCE', 'FUNCTION', 'PROCEDURE', 'AGGREGATE', 'TYPE', 'DOMAIN', 'VIEW']) assert.ok(c.includes(`$t$${kind}$t$`) || c.includes(`ALTER ${kind} `), `hands over ${kind}s`);
|
|
3381
|
+
assert.match(c, /OWNER TO %I/);
|
|
3382
|
+
assert.match(c, /\$r\$bongos-mercury\$r\$/, 'to the instance\'s own role');
|
|
3383
|
+
assert.match(c, /deptype IN \(\$e\$e\$e\$\)/, 'skips objects an EXTENSION owns — altering those errors, and they are not the project\'s');
|
|
3384
|
+
assert.match(c, /\$s\$public\$s\$::regnamespace/, 'only the public schema');
|
|
3385
|
+
assert.doesNotMatch(c, /REASSIGN OWNED/, 'REASSIGN OWNED BY postgres would also hand over every SIBLING database postgres owns');
|
|
3386
|
+
});
|
|
3387
|
+
|
|
3388
|
+
await ta('provisionInstance: hands ownership over right after the grants, and a failure there does not fail the provision (task 1004735)', async () => {
|
|
3389
|
+
const { deps } = recorder();
|
|
3390
|
+
const seen = [];
|
|
3391
|
+
deps.exec = (cmd, opts = {}) => { seen.push({ cmd, opts }); return { ok: true, stdout: '' }; };
|
|
3392
|
+
await P.provisionInstance(coTenant, deps);
|
|
3393
|
+
const grants = seen.findIndex((c) => c.cmd.includes('GRANT ALL ON SCHEMA public'));
|
|
3394
|
+
const own = seen.findIndex((c) => c.cmd.includes('OWNER TO %I') && c.cmd.includes('regnamespace'));
|
|
3395
|
+
assert.ok(grants >= 0 && own === grants + 1, `the handover follows the grants (grants at ${grants}, handover at ${own})`);
|
|
3396
|
+
assert.equal(seen[own].opts.allowFail, true, 'non-fatal: the migrate_not_owner remedy is the backstop');
|
|
3397
|
+
});
|
|
3398
|
+
|
|
3399
|
+
t('dbOwnershipSql: the SQL body carries no quote of either kind, so it survives psql -c and bash -s', () => {
|
|
3400
|
+
const sql = PR.dbOwnershipSql('bongos-mercury');
|
|
3401
|
+
assert.ok(!sql.includes("'"), sql);
|
|
3402
|
+
assert.ok(!sql.includes(String.fromCharCode(34)), sql);
|
|
3403
|
+
assert.ok(!sql.includes('$p$'), 'never the password marker the runner log redaction keys on');
|
|
3404
|
+
assert.throws(() => PR.dbOwnershipSql('x; drop'), /role/, 'a role name that is not a plain identifier is refused, never quoted');
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3373
3407
|
t('the role + seed commands render with NO double quote — they must survive `bash -s` over SSH unchanged', () => {
|
|
3374
3408
|
const dq = String.fromCharCode(34);
|
|
3375
3409
|
for (const c of [P.dbRoleCmd(coTenant, { password: 'pw_ABC-123' }), P.dbRoleGrantsCmd(coTenant), P.seedFirstVersionCmd(coTenant)]) {
|
|
@@ -254,6 +254,16 @@ test('a full upgrade whose migrate fails reports the migration, the error, and a
|
|
|
254
254
|
assert.equal(outcome.classifyFailure('migrate_check_violation').class, 'unknown', 'existing rows break the new rule: a person decides, never an unattended retry');
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
test('"must be owner of" in a failed migrate is the known not-owner snag, not an unknown failure (task 1004735)', () => {
|
|
258
|
+
const outcome = require('../scripts/gds/upgrade-outcome.js');
|
|
259
|
+
const res = { ok: false, rolledBack: true, rollback: {}, error: 'migrate failed (exit 3) at core_274_parked_backlog_stays_parked: ERROR: must be owner of table tasks' };
|
|
260
|
+
assert.equal(outcome.upgradeReason(res), 'migrate_not_owner');
|
|
261
|
+
assert.deepEqual(outcome.classifyFailure('migrate_not_owner'), { class: 'known_wedge', reason: 'migrate_not_owner' });
|
|
262
|
+
const fn = { ...res, error: 'migrate failed (exit 3) at core_274_x: ERROR: must be owner of function promote_dependents_on_ship' };
|
|
263
|
+
assert.equal(outcome.upgradeReason(fn), 'migrate_not_owner', 'functions too — CREATE OR REPLACE on a postgres-owned function');
|
|
264
|
+
assert.equal(outcome.upgradeReason({ ...res, error: 'migrate failed (exit 3) at core_5: ERROR: boom' }), 'migrate', 'any other migrate failure is unchanged');
|
|
265
|
+
});
|
|
266
|
+
|
|
257
267
|
test('teeSpawn streams the child\'s output live AND keeps it for the failure report', async () => {
|
|
258
268
|
const pipe = () => Object.assign(new EventEmitter(), { setEncoding() {} });
|
|
259
269
|
let child;
|
package/tests/wedge_remedy.mjs
CHANGED
|
@@ -52,7 +52,7 @@ function harness({ execFails = false, privileged = false } = {}) {
|
|
|
52
52
|
console.log('\nthe fix for each known snag (planRemedy):');
|
|
53
53
|
|
|
54
54
|
await t('every reason with a fix is a known_wedge in the classifier — and back', () => {
|
|
55
|
-
assert.deepEqual([...W.REMEDY_REASONS].sort(), ['dirty_tree', 'disk_full', 'schema_pending']);
|
|
55
|
+
assert.deepEqual([...W.REMEDY_REASONS].sort(), ['dirty_tree', 'disk_full', 'migrate_not_owner', 'schema_pending']);
|
|
56
56
|
for (const r of W.REMEDY_REASONS) assert.equal(REASON_CLASS[r], 'known_wedge', r);
|
|
57
57
|
for (const [r, c] of Object.entries(REASON_CLASS)) if (c === 'known_wedge') assert.ok(W.REMEDY_REASONS.includes(r), `${r} is known_wedge but has no fix — it would be retired with nothing done`);
|
|
58
58
|
assert.equal(REASON_CLASS.remedy_did_not_hold, 'unknown', 'a fix that did not hold reaches a person');
|
|
@@ -92,6 +92,28 @@ await t('schema pending: migrates the project\'s OWN database, PGDATABASE always
|
|
|
92
92
|
assert.ok(W.planRemedy(inst(), 'schema_pending', { selfRoot, privileged: true, runAs: null }).refused);
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
await t('migrate not owner: hands the project\'s tables to ITS OWN account, as postgres (task 1004735)', () => {
|
|
96
|
+
const p = W.planRemedy(inst(), 'migrate_not_owner', { selfRoot, privileged: true });
|
|
97
|
+
assert.match(p.cmd, /^sudo -u postgres psql -v ON_ERROR_STOP=1 -d charter -c '/, 'only postgres can give away what postgres owns, and only in THIS project\'s database');
|
|
98
|
+
assert.match(p.cmd, /OWNER TO %I/);
|
|
99
|
+
assert.match(p.cmd, /\$r\$bongos-charter\$r\$/, 'to the account the move migrates as');
|
|
100
|
+
assert.match(p.describe, /bongos-charter/);
|
|
101
|
+
const plain = W.planRemedy(inst(), 'migrate_not_owner', { selfRoot });
|
|
102
|
+
assert.match(plain.cmd, /^psql -v ON_ERROR_STOP=1 -d charter -c '/);
|
|
103
|
+
assert.ok(W.planRemedy(inst({ hosting_shape: 'control-plane' }), 'migrate_not_owner', { selfRoot }).refused, 'the control plane migrates as its own app user — there is no project account to hand anything to');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await t('migrate not owner: remedied once, then the move is tried again', async () => {
|
|
107
|
+
const h = harness({ privileged: true });
|
|
108
|
+
const r = await W.remedyAfterFailure({ intent: intent(), inst: inst(), result: wedge('migrate_not_owner'), deps: h.deps });
|
|
109
|
+
assert.deepEqual(r, { repended: true });
|
|
110
|
+
assert.equal(h.ran.length, 1);
|
|
111
|
+
assert.match(h.ran[0].cmd, /^sudo -u postgres psql /);
|
|
112
|
+
const again = await W.remedyAfterFailure({ intent: intent({ remedied_reason: 'migrate_not_owner' }), inst: inst(), result: wedge('migrate_not_owner'), deps: harness({ privileged: true }).deps });
|
|
113
|
+
assert.equal(again.repended, false, 'a handover that did not clear it reaches a person');
|
|
114
|
+
assert.equal(again.failure.reason, 'remedy_did_not_hold');
|
|
115
|
+
});
|
|
116
|
+
|
|
95
117
|
await t('no command is built from a name that is not shell-safe', () => {
|
|
96
118
|
assert.ok(W.planRemedy(inst({ slug: 'a;rm -rf' }), 'dirty_tree', { selfRoot }).refused);
|
|
97
119
|
assert.ok(W.planRemedy(inst({ db_name: 'x; drop' }), 'disk_full', { selfRoot }).refused);
|