@bongos/core 1.19.998 → 1.19.1000
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 +105 -90
- package/.claude/skills/collab-review/SKILL.md +24 -13
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0330-an-ask-is-answered-where-it-was-asked.md +155 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +154 -4
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +18 -13
- package/docs/copy-registry.json +70 -15
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/collab-lib.js +79 -0
- package/modules/hall-ui/public/collab.css +60 -0
- package/modules/hall-ui/public/collab.js +134 -5
- package/modules/lifecycle/help-requests.js +118 -8
- package/modules/lifecycle/migrations/lifecycle_011_help_request_replies.sql +113 -0
- package/modules/lifecycle/routes/help-requests.js +93 -13
- package/modules/provisioning/capacity.js +3 -3
- package/modules/provisioning/control-plane-guard.js +1 -1
- package/modules/provisioning/core-upgrade.js +7 -7
- package/modules/provisioning/migrations/provisioning_027_cloud_host_shape_rename.sql +58 -0
- package/modules/provisioning/onboard-plan.js +1 -1
- package/modules/provisioning/paid-shape-gate.js +28 -15
- package/modules/provisioning/provisioning.js +18 -14
- package/modules/provisioning/routes/repo-private.js +1 -1
- package/modules/provisioning/tests/liveness.mjs +16 -8
- package/modules/provisioning/tests/provisioning.mjs +1 -1
- package/modules/public-landing/public/projects.html +26 -27
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/init.js +12 -4
- package/scripts/gds/onboard.js +1 -1
- package/scripts/gds/provision-capacity.js +6 -1
- package/scripts/gds/provision-config.js +6 -3
- package/scripts/gds/provision-core-upgrade.js +8 -8
- package/scripts/gds/provision-disconnect.js +2 -2
- package/scripts/gds/provision-process.js +2 -2
- package/scripts/gds/provision-repo-private.js +1 -1
- package/scripts/gds/provision-repo.js +13 -3
- package/scripts/gds/provision-teardown.js +2 -2
- package/scripts/gds/provision-units.js +2 -2
- package/scripts/gds/provision.js +6 -6
- package/src/module-api.js +1 -1
- package/tests/collab_page.mjs +140 -7
- package/tests/core_upgrade_door.mjs +1 -1
- package/tests/core_upgrade_runner.mjs +20 -20
- package/tests/deploy_page_projects.mjs +5 -5
- package/tests/hall_collab_world.mjs +6 -2
- package/tests/help_requests.mjs +201 -6
- package/tests/hub_disconnect_ui.mjs +4 -1
- package/tests/init.mjs +6 -6
- package/tests/onboard.mjs +5 -4
- package/tests/project_catalog_backfill.mjs +1 -1
- package/tests/project_catalog_bridge.mjs +2 -2
- package/tests/project_catalog_routes.mjs +4 -4
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_pre_uat.mjs +22 -5
- package/tests/projects_hub_repo_private.mjs +1 -1
- package/tests/provision.mjs +32 -32
- package/tests/provision_byo_host.mjs +1 -1
- package/tests/provision_caddy_wiring.mjs +1 -1
- package/tests/provision_disconnect.mjs +3 -3
- package/tests/provision_repo_private.mjs +1 -1
- package/tests/provision_restart.mjs +5 -5
- package/tests/provision_settings_apply.mjs +4 -4
- package/tests/provision_teardown_address.mjs +2 -2
- package/tests/provisioning_capacity.mjs +40 -34
- package/tests/provisioning_capacity_gate.mjs +5 -12
- package/tests/provisioning_default_domain.mjs +1 -1
- package/tests/provisioning_domain_attach.mjs +4 -4
- package/tests/provisioning_domain_unique.mjs +2 -2
- package/tests/provisioning_error_note_scope.mjs +1 -1
- package/tests/provisioning_module_selection.mjs +5 -5
- package/tests/provisioning_onboard_mode.mjs +2 -2
- package/tests/provisioning_paid_shape_gate.mjs +57 -44
- package/tests/provisioning_repo_private_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +11 -4
- package/tests/provisioning_settings_apply.mjs +12 -5
- package/tests/provisioning_settings_env.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +3 -3
- package/tests/public_landing_projects.mjs +13 -6
- package/tests/upgrade.mjs +2 -2
package/tests/provision.mjs
CHANGED
|
@@ -108,7 +108,7 @@ t('serviceUnit binds the instance port + DB + per-instance env file, runs platfo
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
t('serviceUnit(standalone) runs the CORE PACKAGE server from the instance\'s OWN repo dir (ADR 0108)', () => {
|
|
111
|
-
const inst = { ...coTenant, hosting_shape: '
|
|
111
|
+
const inst = { ...coTenant, hosting_shape: 'cloud-host' };
|
|
112
112
|
const u = P.serviceUnit(inst, { etcBase: '/etc' });
|
|
113
113
|
const root = P.standaloneRoot(inst);
|
|
114
114
|
assert.match(u, new RegExp('WorkingDirectory=' + reEsc(root)), 'runs from the instance\'s own repo dir');
|
|
@@ -118,7 +118,7 @@ t('serviceUnit(standalone) runs the CORE PACKAGE server from the instance\'s OWN
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
t('standalone step generators: root under standaloneBase + pull / npm-ci / instance-root migrate', () => {
|
|
121
|
-
const inst = { ...coTenant, hosting_shape: '
|
|
121
|
+
const inst = { ...coTenant, hosting_shape: 'cloud-host' };
|
|
122
122
|
assert.match(P.standaloneRoot(inst), /\/mercury$/);
|
|
123
123
|
assert.equal(P.standalonePullCmd(), 'git pull --ff-only origin main');
|
|
124
124
|
assert.equal(P.standaloneInstallCmd(), 'npm ci --omit=dev');
|
|
@@ -220,7 +220,7 @@ t('backup units: per-instance names + the DB set on the unit (fixes the task-221
|
|
|
220
220
|
|
|
221
221
|
t('backupScriptPath resolves the CONSUMER LAYOUT, never the old /home/lars/example monorepo path', () => {
|
|
222
222
|
// standalone → the script inside the instance's OWN pinned @bongos/core
|
|
223
|
-
const standalone = { ...coTenant, hosting_shape: '
|
|
223
|
+
const standalone = { ...coTenant, hosting_shape: 'cloud-host' };
|
|
224
224
|
const sPath = P.backupScriptPath(standalone);
|
|
225
225
|
assert.ok(sPath.startsWith(P.standaloneRoot(standalone) + '/'), 'under the instance root');
|
|
226
226
|
assert.match(sPath, /node_modules\/@bongos\/core\/scripts\/gds\/db-backup-nightly\.sh$/);
|
|
@@ -311,7 +311,7 @@ t('webEnvBody: the repo target_ref seats the founding owner via <prefix>_FIRST_A
|
|
|
311
311
|
// seat an Archon on first sign-in and the owner can't claim their own instance. Derived
|
|
312
312
|
// from the repo owner in target_ref — the SAME source instanceInitSpec uses for the
|
|
313
313
|
// scaffold's branding.firstAdmin, so the env override and config/branding.json agree.
|
|
314
|
-
const b = P.webEnvBody({ ...coTenant, hosting_shape: '
|
|
314
|
+
const b = P.webEnvBody({ ...coTenant, hosting_shape: 'cloud-host', domain: 'mercury.example.com', target_ref: 'octocat/mercury' });
|
|
315
315
|
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=octocat`));
|
|
316
316
|
// No target_ref ⇒ the line is omitted (org-owned or unknown owner sets it by hand).
|
|
317
317
|
assert.doesNotMatch(P.webEnvBody(coTenant), /_FIRST_ADMIN=/);
|
|
@@ -320,7 +320,7 @@ t('webEnvBody: the repo target_ref seats the founding owner via <prefix>_FIRST_A
|
|
|
320
320
|
t('webEnvBody: a resolved owner login wins over the repo owner — an org-owned repo seats the signed-in builder (task 1003240)', () => {
|
|
321
321
|
// The org that owns an adopted repo is not a sign-in identity; seating it seats nobody.
|
|
322
322
|
// Same preference instanceInitSpec already applies to the scaffold's branding.firstAdmin.
|
|
323
|
-
const inst = { ...coTenant, hosting_shape: '
|
|
323
|
+
const inst = { ...coTenant, hosting_shape: 'cloud-host', domain: 'mercury.example.com', target_ref: 'acme-org/mercury' };
|
|
324
324
|
const withLogin = P.webEnvBody(inst, { ownerLogin: 'octocat' });
|
|
325
325
|
assert.match(withLogin, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=octocat`));
|
|
326
326
|
assert.doesNotMatch(withLogin, /_FIRST_ADMIN=acme-org/);
|
|
@@ -491,7 +491,7 @@ await ta('provisionInstance(standalone): the backup unit ExecStart points at the
|
|
|
491
491
|
// capture the unit BODIES, not just paths, so we can assert the ExecStart path
|
|
492
492
|
const bodies = {};
|
|
493
493
|
deps.writeFile = (p, body) => { writes.push(p); bodies[p] = body; return { ok: true }; };
|
|
494
|
-
const inst = { ...coTenant, hosting_shape: '
|
|
494
|
+
const inst = { ...coTenant, hosting_shape: 'cloud-host', target_ref: 'example-owner/mercury' };
|
|
495
495
|
await P.provisionInstance(inst, deps);
|
|
496
496
|
const svc = bodies[P.backupServicePath(inst)];
|
|
497
497
|
assert.ok(svc, 'wrote the backup service unit');
|
|
@@ -506,7 +506,7 @@ await ta('provisionInstance(standalone): web.env seats the signed-in builder, no
|
|
|
506
506
|
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
507
507
|
// the builders lookup answers with the signed-in owner; every other query stays empty
|
|
508
508
|
deps.db = { query: async (sql) => (/FROM builders/.test(String(sql)) ? { rows: [{ github_login: 'octocat' }] } : { rows: [] }) };
|
|
509
|
-
const inst = { ...coTenant, hosting_shape: '
|
|
509
|
+
const inst = { ...coTenant, hosting_shape: 'cloud-host', target_ref: 'acme-org/mercury', owner_builder_id: 7 };
|
|
510
510
|
const r = await P.provisionInstance(inst, deps);
|
|
511
511
|
assert.equal(r.ok, true);
|
|
512
512
|
const envPath = Object.keys(bodies).find((p) => p.endsWith('/web.env'));
|
|
@@ -517,7 +517,7 @@ await ta('provisionInstance(standalone): web.env seats the signed-in builder, no
|
|
|
517
517
|
|
|
518
518
|
await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + npm ci) + migrates from the instance root', async () => {
|
|
519
519
|
const { deps, cmds, writes } = recorder();
|
|
520
|
-
const r = await P.provisionInstance({ ...coTenant, hosting_shape: '
|
|
520
|
+
const r = await P.provisionInstance({ ...coTenant, hosting_shape: 'cloud-host', target_ref: 'example-owner/mercury' }, deps);
|
|
521
521
|
assert.equal(r.ok, true);
|
|
522
522
|
const joined = cmds.join('\n');
|
|
523
523
|
// scaffold leg (task 2055): git init → package-core → init --vendor-core → push
|
|
@@ -612,7 +612,7 @@ function recordIo({ sudoP = '' } = {}) {
|
|
|
612
612
|
log: (m) => logs.push(m),
|
|
613
613
|
};
|
|
614
614
|
}
|
|
615
|
-
const INST = { slug: 'demo', hosting_shape: '
|
|
615
|
+
const INST = { slug: 'demo', hosting_shape: 'cloud-host', port: 3003, db_name: 'demo' };
|
|
616
616
|
|
|
617
617
|
t('resolveProcessManager: explicit knob wins, then the /run/systemd/system probe', () => {
|
|
618
618
|
const booted = { existsSync: (p) => p === '/run/systemd/system' };
|
|
@@ -729,7 +729,7 @@ t('instanceInitSpec: falls back to the repo owner as firstAdmin when no owner lo
|
|
|
729
729
|
await ta('scaffoldStandaloneRepo: refuses when target_ref is not owner/repo', async () => {
|
|
730
730
|
const { deps } = recorder({ apply: true });
|
|
731
731
|
await assert.rejects(
|
|
732
|
-
() => P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: '
|
|
732
|
+
() => P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'cloud-host', target_ref: null }, deps),
|
|
733
733
|
/target_ref .* is not an owner\/repo/,
|
|
734
734
|
);
|
|
735
735
|
});
|
|
@@ -743,7 +743,7 @@ await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries pac
|
|
|
743
743
|
writeFileSync(join(tmp, 'demo', 'package.json'), '{}\n'); // sign of a completed scaffold
|
|
744
744
|
const { deps, cmds } = recorder({ apply: true });
|
|
745
745
|
deps.exec = (cmd) => { cmds.push(cmd); return { ok: true, stdout: /ls-remote/.test(cmd) ? 'abc123\trefs/heads/main\n' : '' }; };
|
|
746
|
-
const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: '
|
|
746
|
+
const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo' }, deps);
|
|
747
747
|
assert.equal(res.already, true);
|
|
748
748
|
assert.deepEqual(cmds.filter((c) => !/ls-remote/.test(c)), [], 'no init/build/push when already scaffolded AND the remote carries main');
|
|
749
749
|
} finally {
|
|
@@ -770,7 +770,7 @@ async function rerunScaffold({ apply = true, lsRemote, token = null, credWriteTh
|
|
|
770
770
|
writes.push({ p, body, opts }); return { ok: true };
|
|
771
771
|
};
|
|
772
772
|
if (token) deps.getGithubToken = async () => ({ token, scope: 'repo' });
|
|
773
|
-
const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: '
|
|
773
|
+
const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo', owner_builder_id: 7 }, deps);
|
|
774
774
|
return { res, cmds, writes };
|
|
775
775
|
} finally {
|
|
776
776
|
P.CONFIG.standaloneBase = origBase;
|
|
@@ -829,7 +829,7 @@ async function scaffoldWithEnv(env) {
|
|
|
829
829
|
// the scaffold rebuilds via package-core.js (task 1002275). A consumer-instance root
|
|
830
830
|
// (a real pin) takes the reuse path — covered by its own test below.
|
|
831
831
|
await P.scaffoldStandaloneRepo(
|
|
832
|
-
{ slug: 'demo', hosting_shape: '
|
|
832
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo' },
|
|
833
833
|
{ ...deps, env, instanceRoot: tmp },
|
|
834
834
|
);
|
|
835
835
|
return cmds.join('\n');
|
|
@@ -933,7 +933,7 @@ await ta('scaffoldStandaloneRepo: on a consumer control plane it REUSES the pinn
|
|
|
933
933
|
try {
|
|
934
934
|
const { deps, cmds } = recorder({ apply: true });
|
|
935
935
|
await P.scaffoldStandaloneRepo(
|
|
936
|
-
{ slug: 'demo', hosting_shape: '
|
|
936
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo' },
|
|
937
937
|
{ ...deps, instanceRoot: cp },
|
|
938
938
|
);
|
|
939
939
|
const joined = cmds.join('\n');
|
|
@@ -1073,7 +1073,7 @@ await ta('scaffoldStandaloneRepo: a REGISTRY-pinned consumer control plane PACKS
|
|
|
1073
1073
|
try {
|
|
1074
1074
|
const { deps, cmds } = recorder({ apply: true });
|
|
1075
1075
|
await P.scaffoldStandaloneRepo(
|
|
1076
|
-
{ slug: 'demo', hosting_shape: '
|
|
1076
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo' },
|
|
1077
1077
|
{ ...deps, instanceRoot: cp },
|
|
1078
1078
|
);
|
|
1079
1079
|
const joined = cmds.join('\n');
|
|
@@ -1106,7 +1106,7 @@ await ta('scaffoldStandaloneRepo: pushes via the owner OAuth token (not SSH), to
|
|
|
1106
1106
|
deps.writeFile = (p, body, opts) => { writes.push({ p, body, opts }); return { ok: true }; };
|
|
1107
1107
|
deps.getGithubToken = async () => ({ token: 'gho_SECRET_TOKEN_XYZ', scope: 'public_repo' });
|
|
1108
1108
|
await P.scaffoldStandaloneRepo(
|
|
1109
|
-
{ slug: 'demo', hosting_shape: '
|
|
1109
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo', owner_builder_id: 7 },
|
|
1110
1110
|
{ ...deps, instanceRoot: base },
|
|
1111
1111
|
);
|
|
1112
1112
|
const joined = cmds.join('\n');
|
|
@@ -1134,7 +1134,7 @@ await ta('scaffoldStandaloneRepo: no owner token → falls back to the SSH origi
|
|
|
1134
1134
|
try {
|
|
1135
1135
|
const { deps, cmds } = recorder({ apply: true }); // default getGithubToken → null
|
|
1136
1136
|
await P.scaffoldStandaloneRepo(
|
|
1137
|
-
{ slug: 'demo', hosting_shape: '
|
|
1137
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo', owner_builder_id: 7 },
|
|
1138
1138
|
{ ...deps, instanceRoot: base },
|
|
1139
1139
|
);
|
|
1140
1140
|
const joined = cmds.join('\n');
|
|
@@ -1204,7 +1204,7 @@ async function scaffoldInMode(mode, { token = null, seedRoot = null } = {}) {
|
|
|
1204
1204
|
deps.writeFile = (p, body, opts) => { writes.push({ p, body, opts }); return { ok: true }; };
|
|
1205
1205
|
if (token) deps.getGithubToken = async () => ({ token, scope: 'read:user,repo' });
|
|
1206
1206
|
await P.scaffoldStandaloneRepo(
|
|
1207
|
-
{ slug: 'demo', hosting_shape: '
|
|
1207
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo', owner_builder_id: 7, onboard_mode: mode },
|
|
1208
1208
|
{ ...deps, instanceRoot: base },
|
|
1209
1209
|
);
|
|
1210
1210
|
return { joined: cmds.join('\n'), cmds, writes, root: join(base, 'demo') };
|
|
@@ -1279,7 +1279,7 @@ await ta('scaffoldStandaloneRepo(adopt): a default branch that is not `main` fai
|
|
|
1279
1279
|
deps.exec = (cmd) => ({ ok: true, stdout: /rev-parse --abbrev-ref HEAD/.test(cmd) ? 'master\n' : '' });
|
|
1280
1280
|
await assert.rejects(
|
|
1281
1281
|
() => P.scaffoldStandaloneRepo(
|
|
1282
|
-
{ slug: 'demo', hosting_shape: '
|
|
1282
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo', owner_builder_id: 7, onboard_mode: 'adopt' },
|
|
1283
1283
|
{ ...deps, instanceRoot: base },
|
|
1284
1284
|
),
|
|
1285
1285
|
/default branch is "master", not "main"/,
|
|
@@ -1304,7 +1304,7 @@ await ta('scaffoldStandaloneRepo(adopt): the credential file is removed even whe
|
|
|
1304
1304
|
};
|
|
1305
1305
|
await assert.rejects(
|
|
1306
1306
|
() => P.scaffoldStandaloneRepo(
|
|
1307
|
-
{ slug: 'demo', hosting_shape: '
|
|
1307
|
+
{ slug: 'demo', hosting_shape: 'cloud-host', target_ref: 'example-owner/demo', owner_builder_id: 7, onboard_mode: 'adopt' },
|
|
1308
1308
|
{ ...deps, instanceRoot: base },
|
|
1309
1309
|
),
|
|
1310
1310
|
/non-fast-forward/,
|
|
@@ -1720,7 +1720,7 @@ t('migrateCmd: privileged runs migrate as postgres via env+bash (survives sudo e
|
|
|
1720
1720
|
});
|
|
1721
1721
|
|
|
1722
1722
|
t('standaloneMigrateCmd: privileged runs the instance-root migrate as postgres; default unchanged', () => {
|
|
1723
|
-
const inst = { ...coTenant, hosting_shape: '
|
|
1723
|
+
const inst = { ...coTenant, hosting_shape: 'cloud-host' };
|
|
1724
1724
|
assert.equal(P.standaloneMigrateCmd(inst), 'PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
|
|
1725
1725
|
assert.equal(P.standaloneMigrateCmd(inst, { privileged: true }), 'sudo -u postgres env PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules/@bongos/core/scripts/migrate.sh');
|
|
1726
1726
|
});
|
|
@@ -1950,7 +1950,7 @@ await ta('provisionInstance(standalone, remote): EVERY box step runs control-pla
|
|
|
1950
1950
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
|
|
1951
1951
|
checkCaddyWiring: () => null,
|
|
1952
1952
|
};
|
|
1953
|
-
await P.provisionInstance({ ...coTenant, hosting_shape: '
|
|
1953
|
+
await P.provisionInstance({ ...coTenant, hosting_shape: 'cloud-host', target_ref: 'example-owner/mercury', domain: 'mercury.cloudbongos.com' }, deps);
|
|
1954
1954
|
// The bug (task 2074): these landed on the REMOTE box, which never had the scaffolded
|
|
1955
1955
|
// dir. They must ALL be on the control-plane executor now.
|
|
1956
1956
|
for (const re of [/git pull --ff-only/, /npm ci/, /createdb mercury/, /migrate\.sh/, /systemctl enable --now/, /systemctl reload caddy/, /healthz/]) {
|
|
@@ -1991,7 +1991,7 @@ await ta('provisionInstance(standalone, remote, --apply): FAILS LOUD when the co
|
|
|
1991
1991
|
checkCaddyWiring: () => null,
|
|
1992
1992
|
};
|
|
1993
1993
|
await assert.rejects(
|
|
1994
|
-
() => P.provisionInstance({ ...coTenant, hosting_shape: '
|
|
1994
|
+
() => P.provisionInstance({ ...coTenant, hosting_shape: 'cloud-host', target_ref: 'example-owner/mercury', domain: 'mercury.cloudbongos.com' }, deps),
|
|
1995
1995
|
/PROVISION_CONTROL_PLANE_IP/,
|
|
1996
1996
|
'must demand the control-plane IP, not fall back to the co-hosting box',
|
|
1997
1997
|
);
|
|
@@ -2008,7 +2008,7 @@ await ta('teardownInstance(standalone, remote): tears down on the CONTROL PLANE
|
|
|
2008
2008
|
controlExec: (cmd) => { control.push(cmd); return { ok: true }; },
|
|
2009
2009
|
writeFile: () => ({ ok: true }), controlWriteFile: () => ({ ok: true }), doApi: null,
|
|
2010
2010
|
};
|
|
2011
|
-
await P.teardownInstance({ ...coTenant, hosting_shape: '
|
|
2011
|
+
await P.teardownInstance({ ...coTenant, hosting_shape: 'cloud-host' }, deps);
|
|
2012
2012
|
assert.ok(control.some((c) => /systemctl disable --now mercury/.test(c)), 'disable runs on the control plane');
|
|
2013
2013
|
assert.ok(control.some((c) => /rm -f .*mercury\.service/.test(c)), 'unit removed on the control plane');
|
|
2014
2014
|
assert.equal(box.length, 0, 'nothing crosses to the co-hosting box for a standalone teardown');
|
|
@@ -2058,7 +2058,7 @@ t('shouldFederate: dormant unless hub-configured AND co-tenant AND a domain is s
|
|
|
2058
2058
|
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: fakePI(), hubKeys: hubKeysOff }).federate, false, 'no signing key → skip');
|
|
2059
2059
|
assert.equal(P.shouldFederate({ ...coTenantWithDomain, domain: null }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'no domain → skip');
|
|
2060
2060
|
// a standalone (owner-hosted) keeps its own GitHub app; a dedicated droplet has no local web.env bake yet
|
|
2061
|
-
assert.equal(P.shouldFederate({ ...coTenantWithDomain, hosting_shape: '
|
|
2061
|
+
assert.equal(P.shouldFederate({ ...coTenantWithDomain, hosting_shape: 'cloud-host' }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'standalone = self-hosted → skip');
|
|
2062
2062
|
assert.equal(P.shouldFederate({ ...coTenantWithDomain, hosting_shape: 'dedicated' }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'dedicated → skip (no local web.env yet)');
|
|
2063
2063
|
});
|
|
2064
2064
|
// no hub origin resolvable (even with a signing key) → skip
|
|
@@ -2183,8 +2183,8 @@ await ta('provisionInstance(standalone, hub-configured): still NOT federated —
|
|
|
2183
2183
|
deps.controlWriteFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
2184
2184
|
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
2185
2185
|
deps.platformIdentity = pi; deps.hubKeys = hubKeysOn;
|
|
2186
|
-
await P.provisionInstance({ ...coTenantWithDomain, hosting_shape: '
|
|
2187
|
-
const env = bodies[P.webEnvPath({ ...coTenantWithDomain, hosting_shape: '
|
|
2186
|
+
await P.provisionInstance({ ...coTenantWithDomain, hosting_shape: 'cloud-host', target_ref: 'example-owner/mercury' }, deps);
|
|
2187
|
+
const env = bodies[P.webEnvPath({ ...coTenantWithDomain, hosting_shape: 'cloud-host' })];
|
|
2188
2188
|
assert.ok(env, 'wrote web.env');
|
|
2189
2189
|
assert.doesNotMatch(env, /_AUTH_IDP=/, 'standalone is the self-hosted path — no federation');
|
|
2190
2190
|
assert.equal(pi.calls.length, 0, 'no hub registration for a standalone');
|
|
@@ -2239,7 +2239,7 @@ function attachRecorder({ apply = false, envBody = WEB_ENV_ON_BOX } = {}) {
|
|
|
2239
2239
|
}
|
|
2240
2240
|
|
|
2241
2241
|
const activeStandalone = {
|
|
2242
|
-
id: 18, slug: 'spike', hosting_shape: '
|
|
2242
|
+
id: 18, slug: 'spike', hosting_shape: 'cloud-host', status: 'active', owner_builder_id: 7,
|
|
2243
2243
|
port: 3010, db_name: 'spike', domain: 'spike.cloudbongos.com', target_ref: 'jaxri/spike-harbor',
|
|
2244
2244
|
};
|
|
2245
2245
|
|
|
@@ -2728,7 +2728,7 @@ function repoAuthDeps({ apply = true, priv = true, token = 'gho_TOK', keys = [],
|
|
|
2728
2728
|
deps.fsImpl = { existsSync: () => existing, readFileSync: () => `${pub}\n` };
|
|
2729
2729
|
return { deps, cmds, apiCalls };
|
|
2730
2730
|
}
|
|
2731
|
-
const privInst = { id: 9, slug: 'demo', hosting_shape: '
|
|
2731
|
+
const privInst = { id: 9, slug: 'demo', hosting_shape: 'cloud-host', status: 'requested', owner_builder_id: 7, target_ref: 'example-owner/demo' };
|
|
2732
2732
|
|
|
2733
2733
|
await ta('ensurePrivateRepoAccess(private): registers a READ-ONLY key and wires origin + core.sshCommand into the checkout', async () => {
|
|
2734
2734
|
const { deps, cmds, apiCalls } = repoAuthDeps({ existing: false });
|
|
@@ -2965,7 +2965,7 @@ await ta('intendedPrivate(dry-run): names the pre-flip intent in the plan line,
|
|
|
2965
2965
|
|
|
2966
2966
|
await ta('provisionInstance(standalone): the private-repo wiring lands BETWEEN the scaffold and the step-2 pull', async () => {
|
|
2967
2967
|
const { deps, cmds } = repoAuthDeps({ existing: false });
|
|
2968
|
-
const r = await P.provisionInstance({ ...coTenant, hosting_shape: '
|
|
2968
|
+
const r = await P.provisionInstance({ ...coTenant, hosting_shape: 'cloud-host', target_ref: 'example-owner/mercury' }, deps);
|
|
2969
2969
|
assert.equal(r.ok, true);
|
|
2970
2970
|
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
2971
2971
|
assert.ok(idx(/remote set-url origin git@github\.com:example-owner\/mercury\.git/) >= 0, 'wired the private origin');
|
|
@@ -3147,7 +3147,7 @@ t('serviceUnit: carries the hardening block, with HOME + ReadWritePaths pointing
|
|
|
3147
3147
|
});
|
|
3148
3148
|
|
|
3149
3149
|
t('serviceUnit: ReadWritePaths does NOT make the checkout the service runs writable by it', () => {
|
|
3150
|
-
const inst = { ...coTenant, hosting_shape: '
|
|
3150
|
+
const inst = { ...coTenant, hosting_shape: 'cloud-host' };
|
|
3151
3151
|
const u = P.serviceUnit(inst, { etcBase: '/etc' });
|
|
3152
3152
|
const root = P.standaloneRoot(inst);
|
|
3153
3153
|
const rw = u.split('\n').find((l) => l.startsWith('ReadWritePaths='));
|
|
@@ -3171,7 +3171,7 @@ t('webEnvBody: NO password ⇒ no PG credential lines — an instance provisione
|
|
|
3171
3171
|
|
|
3172
3172
|
// ── the Postgres pool cap (task 1003928, ADR 0285 §3) ─────────────────────────
|
|
3173
3173
|
t('webEnvBody: a CO-LOCATED instance (co-tenant or standalone) carries PGPOOL_MAX at the runner\'s cap, beside PGDATABASE', () => {
|
|
3174
|
-
for (const shape of ['co-tenant', '
|
|
3174
|
+
for (const shape of ['co-tenant', 'cloud-host']) {
|
|
3175
3175
|
const b = P.webEnvBody({ ...coTenant, hosting_shape: shape });
|
|
3176
3176
|
assert.match(b, new RegExp(`^PGPOOL_MAX=${P.CONFIG.instancePoolMax}$`, 'm'), `${shape} carries the cap`);
|
|
3177
3177
|
const lines = b.split('\n');
|
|
@@ -115,7 +115,7 @@ test('the shape is absent from the runner shapes that DO get stood up', () => {
|
|
|
115
115
|
// Any other shape still reaches the first executor — that is the control.
|
|
116
116
|
const h = harness();
|
|
117
117
|
return assert.rejects(
|
|
118
|
-
P.provisionInstance({ ...byoHost, hosting_shape: '
|
|
118
|
+
P.provisionInstance({ ...byoHost, hosting_shape: 'cloud-host', domain: null }, h.deps),
|
|
119
119
|
/byo-host must not/,
|
|
120
120
|
'a non-byo-host shape still reaches the box steps — the branch is shape-scoped, not a global off switch',
|
|
121
121
|
);
|
|
@@ -58,7 +58,7 @@ test('output that is not Caddy JSON, or a failed adapt, is a gap rather than a p
|
|
|
58
58
|
// -- the runner legs --------------------------------------------------------
|
|
59
59
|
|
|
60
60
|
const coTenant = { id: 1, slug: 'mercury', hosting_shape: 'co-tenant', status: 'requested', owner_builder_id: 7, port: 3003, db_name: 'mercury', domain: DOMAIN };
|
|
61
|
-
const activeStandalone = { id: 18, slug: 'spike', hosting_shape: '
|
|
61
|
+
const activeStandalone = { id: 18, slug: 'spike', hosting_shape: 'cloud-host', status: 'active', owner_builder_id: 7, port: 3010, db_name: 'spike', domain: 'spike.example.com' };
|
|
62
62
|
const WEB_ENV = 'PORT=3010\nBONGOS_PUBLIC_ORIGIN=https://old.example.com\n';
|
|
63
63
|
|
|
64
64
|
// `served` = the hosts Caddy's resolved config carries once a snippet is written — the
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// • ORDER: nothing is erased until the teardown ran; the row is deleted only after every
|
|
14
14
|
// erase and the catalog forget succeeded, so a failure leaves a row the runner can retry
|
|
15
15
|
// • the dump erase matches the nightly filename EXACTLY, so `test` never erases `test-nk`
|
|
16
|
-
// • each shape erases where its data lives (
|
|
16
|
+
// • each shape erases where its data lives (cloud-host: control plane; co-tenant: the
|
|
17
17
|
// co-hosting box; dedicated: nothing — its droplet went whole)
|
|
18
18
|
// • a dry run erases nothing and deletes nothing; an invalid slug is refused before any command
|
|
19
19
|
//
|
|
@@ -45,7 +45,7 @@ async function ta(name, fn) {
|
|
|
45
45
|
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const lantern = { id: 40, slug: 'lantern', hosting_shape: '
|
|
48
|
+
const lantern = { id: 40, slug: 'lantern', hosting_shape: 'cloud-host', status: 'torn_down', owner_builder_id: 7, port: 3021, db_name: 'lantern', domain: 'lantern.cloudbongos.com', target_ref: null };
|
|
49
49
|
|
|
50
50
|
function rig({ apply = true, privileged = false, failOn = null, catalogThrows = false, catalog = true } = {}) {
|
|
51
51
|
const events = [], logs = [];
|
|
@@ -88,7 +88,7 @@ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
|
|
|
88
88
|
|
|
89
89
|
console.log('the erase, in order:');
|
|
90
90
|
|
|
91
|
-
await ta('
|
|
91
|
+
await ta('cloud-host: teardown legs, then the database, its role, the dumps, env + state, checkout, account — then forget, then the row', async () => {
|
|
92
92
|
const r = rig();
|
|
93
93
|
const res = await D.disconnectInstance(lantern, r.deps);
|
|
94
94
|
assert.equal(res.ok, true, JSON.stringify(res));
|
|
@@ -37,7 +37,7 @@ async function ta(name, fn) {
|
|
|
37
37
|
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const liveStandalone = { id: 18, slug: 'demo', hosting_shape: '
|
|
40
|
+
const liveStandalone = { id: 18, slug: 'demo', hosting_shape: 'cloud-host', status: 'active', owner_builder_id: 7, target_ref: 'example-owner/demo' };
|
|
41
41
|
|
|
42
42
|
// One ordered timeline across the shell AND GitHub, so the wire-before-flip order is
|
|
43
43
|
// observable as positions in a single list rather than inferred from two.
|
|
@@ -70,13 +70,13 @@ function recorder({ apply = true, healthzFails = false, privileged = false, rest
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const activeStandalone = {
|
|
73
|
-
id: 18, slug: 'spike', hosting_shape: '
|
|
73
|
+
id: 18, slug: 'spike', hosting_shape: 'cloud-host', status: 'active', owner_builder_id: 7,
|
|
74
74
|
port: 3010, db_name: 'spike', domain: 'spike.cloudbongos.com', target_ref: 'jaxri/spike-harbor',
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
console.log('restartInstance — the leg:');
|
|
78
78
|
|
|
79
|
-
await ta('--apply, active
|
|
79
|
+
await ta('--apply, active cloud-host: ONE systemctl restart, then healthz — and web.env is never read or written', async () => {
|
|
80
80
|
const { deps, cmds, writes } = recorder();
|
|
81
81
|
const r = await P.restartInstance(activeStandalone, deps);
|
|
82
82
|
assert.equal(r.ok, true, JSON.stringify(r));
|
|
@@ -176,9 +176,9 @@ await ta('dry-run runs no real work and observes nothing', async () => {
|
|
|
176
176
|
assert.equal(writes.length, 0);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
await ta('a co-tenant restarts on the co-hosting box; a
|
|
179
|
+
await ta('a co-tenant restarts on the co-hosting box; a cloud-host on the control plane', async () => {
|
|
180
180
|
const seen = {};
|
|
181
|
-
for (const shape of ['co-tenant', '
|
|
181
|
+
for (const shape of ['co-tenant', 'cloud-host']) {
|
|
182
182
|
const box = [], control = [];
|
|
183
183
|
const { deps } = recorder();
|
|
184
184
|
deps.exec = (cmd) => { box.push(cmd); return /healthz/.test(cmd) ? { ok: true, stdout: '{}' } : { ok: true, stdout: '' }; };
|
|
@@ -187,7 +187,7 @@ await ta('a co-tenant restarts on the co-hosting box; a standalone on the contro
|
|
|
187
187
|
seen[shape] = { box: box.length, control: control.length };
|
|
188
188
|
}
|
|
189
189
|
assert.ok(seen['co-tenant'].box > 0 && seen['co-tenant'].control === 0, 'a co-tenant runs on the box');
|
|
190
|
-
assert.ok(seen.
|
|
190
|
+
assert.ok(seen['cloud-host'].control > 0 && seen['cloud-host'].box === 0, 'a cloud-host runs control-plane-local (task 2074) — restart it THERE');
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
await ta('privileged mode escalates the restart', async () => {
|
|
@@ -121,7 +121,7 @@ t('is PURE: same input, same output; empty body yields just the lines', () => {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at standup, under the pack prefix', () => {
|
|
124
|
-
const inst = { slug: 'spike', port: 3010, db_name: 'spike', hosting_shape: '
|
|
124
|
+
const inst = { slug: 'spike', port: 3010, db_name: 'spike', hosting_shape: 'cloud-host', target_ref: 'jaxri/spike', settings: { platform_visibility: 'gated' } };
|
|
125
125
|
const vars = P.settingsEnvVarsFor(inst);
|
|
126
126
|
assert.deepEqual(vars, {
|
|
127
127
|
[`${NEUTRAL_PREFIX}_PLATFORM_VISIBILITY`]: 'gated', [`${NEUTRAL_PREFIX}_JOINABILITY`]: 'apply',
|
|
@@ -199,14 +199,14 @@ function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, health
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
const activeStandalone = {
|
|
202
|
-
id: 18, slug: 'spike', hosting_shape: '
|
|
202
|
+
id: 18, slug: 'spike', hosting_shape: 'cloud-host', status: 'active', owner_builder_id: 7,
|
|
203
203
|
port: 3010, db_name: 'spike', domain: 'spike.cloudbongos.com', target_ref: 'jaxri/spike-harbor',
|
|
204
204
|
settings: { platform_visibility: 'gated', joinability: 'invite_only' },
|
|
205
205
|
};
|
|
206
206
|
|
|
207
207
|
console.log('\nsettingsApplyInstance — the leg:');
|
|
208
208
|
|
|
209
|
-
await ta('--apply, active
|
|
209
|
+
await ta('--apply, active cloud-host: read web.env → patch ONLY the settings lines (control-plane writer) → restart → healthz → manifest; creds survive', async () => {
|
|
210
210
|
const { deps, cmds, writes } = recorder({ apply: true });
|
|
211
211
|
const r = await P.settingsApplyInstance(activeStandalone, deps);
|
|
212
212
|
assert.equal(r.ok, true, JSON.stringify(r));
|
|
@@ -251,7 +251,7 @@ await ta('privileged: the read and the restart escalate via sudo (the /etc file
|
|
|
251
251
|
await ta('co-tenant rides the BOX executor + writer; standalone rides the control-plane pair (task 2074 routing)', async () => {
|
|
252
252
|
const seen = { box: [], control: [] };
|
|
253
253
|
const mk = (bucket) => (cmd) => { seen[bucket].push(cmd); return /\bcat /.test(cmd) ? { ok: true, stdout: LIVE_ENV } : { ok: true, stdout: '' }; };
|
|
254
|
-
for (const [shape, want] of [['co-tenant', 'box'], ['
|
|
254
|
+
for (const [shape, want] of [['co-tenant', 'box'], ['cloud-host', 'control']]) {
|
|
255
255
|
seen.box = []; seen.control = [];
|
|
256
256
|
const { deps, writes } = recorder({ apply: true });
|
|
257
257
|
deps.exec = mk('box'); deps.controlExec = mk('control');
|
|
@@ -32,7 +32,7 @@ async function ta(name, fn) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const HOOK = '/opt/infra/box-dns-cloudflare.sh';
|
|
35
|
-
const lantern = { id: 5, slug: 'lantern', hosting_shape: '
|
|
35
|
+
const lantern = { id: 5, slug: 'lantern', hosting_shape: 'cloud-host', status: 'active', owner_builder_id: 7, port: 3021, db_name: 'lantern', domain: 'lantern.cloudbongos.com', target_ref: null };
|
|
36
36
|
|
|
37
37
|
// One ordered ledger across both executors and the status writes, so ordering is assertable.
|
|
38
38
|
function rig({ apply = true, privileged = false, hookThrows = false } = {}) {
|
|
@@ -67,7 +67,7 @@ async function withHook(hook, fn) {
|
|
|
67
67
|
|
|
68
68
|
console.log('the Caddy leg:');
|
|
69
69
|
|
|
70
|
-
await ta('
|
|
70
|
+
await ta('cloud-host: the <slug>.caddy snippet is removed, THEN Caddy reloads — on the control plane, where it ran', async () => {
|
|
71
71
|
await withHook(HOOK, async () => {
|
|
72
72
|
const r = rig();
|
|
73
73
|
await P.teardownInstance(lantern, r.deps);
|