@bongos/core 1.20.64 → 1.20.65
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 +16 -16
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/npm-release/preview/env.js +10 -1
- package/modules/npm-release/preview/supervisor.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/npm_release_preview_diagnosis.mjs +1 -1
- package/tests/npm_release_preview_env.mjs +23 -0
- package/tests/npm_release_preview_supervisor.mjs +18 -3
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.65",
|
|
6
|
+
"core_contract": "1.20.65",
|
|
7
|
+
"source_commit": "d83ddd9aee9444dee8d60cf40c8dcb9b3d32e296",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-01T11:03:38.854Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 572,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 3390,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "ede972bceb550b8da7d974492895636275f2f66415178c7632b00d1d85e3dbe0",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2207,7 +2207,7 @@
|
|
|
2207
2207
|
{
|
|
2208
2208
|
"path": "docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md",
|
|
2209
2209
|
"mode": "0000644",
|
|
2210
|
-
"sha256": "
|
|
2210
|
+
"sha256": "b609d870b217440a12839ce675617973aac721e9f017e831ff208e06567c9939"
|
|
2211
2211
|
},
|
|
2212
2212
|
{
|
|
2213
2213
|
"path": "docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md",
|
|
@@ -2802,7 +2802,7 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "9754d3deec186136216507d8dfc84c40eaf82c4ca40c63f48ad60884527452bf"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -7142,7 +7142,7 @@
|
|
|
7142
7142
|
{
|
|
7143
7143
|
"path": "modules/npm-release/preview/env.js",
|
|
7144
7144
|
"mode": "0000644",
|
|
7145
|
-
"sha256": "
|
|
7145
|
+
"sha256": "018bdef460587e17cb6bf1fd622a2b14f671a777a1e36e4aeb00e798da5f30c3"
|
|
7146
7146
|
},
|
|
7147
7147
|
{
|
|
7148
7148
|
"path": "modules/npm-release/preview/proxy.js",
|
|
@@ -7157,7 +7157,7 @@
|
|
|
7157
7157
|
{
|
|
7158
7158
|
"path": "modules/npm-release/preview/supervisor.js",
|
|
7159
7159
|
"mode": "0000644",
|
|
7160
|
-
"sha256": "
|
|
7160
|
+
"sha256": "cd708c56ae7bba361c139aba9cfccbe8501843db1c4f54bcd0c0dcf93e76f83c"
|
|
7161
7161
|
},
|
|
7162
7162
|
{
|
|
7163
7163
|
"path": "modules/npm-release/public/task-widget.js",
|
|
@@ -9447,12 +9447,12 @@
|
|
|
9447
9447
|
{
|
|
9448
9448
|
"path": "package-lock.json",
|
|
9449
9449
|
"mode": "0000644",
|
|
9450
|
-
"sha256": "
|
|
9450
|
+
"sha256": "9c2fa6e82319388a9b4b7c68acb6aabf12cf447cc9df068c9351540e39e41f6e"
|
|
9451
9451
|
},
|
|
9452
9452
|
{
|
|
9453
9453
|
"path": "package.json",
|
|
9454
9454
|
"mode": "0000644",
|
|
9455
|
-
"sha256": "
|
|
9455
|
+
"sha256": "253fd4502f205668e930fb1863b1dbc47f376462139e3a33809d84bb62157110"
|
|
9456
9456
|
},
|
|
9457
9457
|
{
|
|
9458
9458
|
"path": "public-docs/index.html",
|
|
@@ -9472,7 +9472,7 @@
|
|
|
9472
9472
|
{
|
|
9473
9473
|
"path": "release-notes.json",
|
|
9474
9474
|
"mode": "0000644",
|
|
9475
|
-
"sha256": "
|
|
9475
|
+
"sha256": "e041a1335bad43a910cce4057eaf312ec70054008fef566a5b07f373fbddd8aa"
|
|
9476
9476
|
},
|
|
9477
9477
|
{
|
|
9478
9478
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -11652,7 +11652,7 @@
|
|
|
11652
11652
|
{
|
|
11653
11653
|
"path": "src/module-api.js",
|
|
11654
11654
|
"mode": "0000644",
|
|
11655
|
-
"sha256": "
|
|
11655
|
+
"sha256": "b8165107a23910ad35f528a86cd39ddff17a185fe7081f7cb8fb03f0c072a813"
|
|
11656
11656
|
},
|
|
11657
11657
|
{
|
|
11658
11658
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14877,7 +14877,7 @@
|
|
|
14877
14877
|
{
|
|
14878
14878
|
"path": "tests/npm_release_preview_diagnosis.mjs",
|
|
14879
14879
|
"mode": "0000644",
|
|
14880
|
-
"sha256": "
|
|
14880
|
+
"sha256": "1d0fe23920e58d8838d6753750097d55c0c36d32d30d8f14ddd916a8bea908d6"
|
|
14881
14881
|
},
|
|
14882
14882
|
{
|
|
14883
14883
|
"path": "tests/npm_release_preview_divert.mjs",
|
|
@@ -14887,7 +14887,7 @@
|
|
|
14887
14887
|
{
|
|
14888
14888
|
"path": "tests/npm_release_preview_env.mjs",
|
|
14889
14889
|
"mode": "0000644",
|
|
14890
|
-
"sha256": "
|
|
14890
|
+
"sha256": "9febec41c155f29a2e22a7fa7dc8059be1fe6a36ff1845dceb85f87e695f42c3"
|
|
14891
14891
|
},
|
|
14892
14892
|
{
|
|
14893
14893
|
"path": "tests/npm_release_preview_proxy.mjs",
|
|
@@ -14902,7 +14902,7 @@
|
|
|
14902
14902
|
{
|
|
14903
14903
|
"path": "tests/npm_release_preview_supervisor.mjs",
|
|
14904
14904
|
"mode": "0000644",
|
|
14905
|
-
"sha256": "
|
|
14905
|
+
"sha256": "f326bc26e3b137dc8e17a22beeb08861181d35dc881ec1762e05377b9d9430e7"
|
|
14906
14906
|
},
|
|
14907
14907
|
{
|
|
14908
14908
|
"path": "tests/npm_release_where.mjs",
|
|
@@ -97,6 +97,7 @@ What keeps that exception narrow:
|
|
|
97
97
|
- A preview started by the live process is stopped when that process exits, but the copy is
|
|
98
98
|
dropped only by the next start or stop. It is one fixed name, so it cannot pile up.
|
|
99
99
|
- **Correction, 2026-09-30 (task 1004466): the proxy strips every upstream `Set-Cookie`.** Same origin means a cookie the child set replaced the live hall's own (`cb_stealth`, the session) and locked the owner out of `/deploy`. The child's cookies now stay with the child; the steering cookie is the only one this module writes, and its clear repeats the set's Path, SameSite and Secure.
|
|
100
|
+
- **Correction, 2026-10-01 (task 1004486): the child is handed `PGUSER`.** The droplet's hall connects by socket peer auth with no `PGUSER`, so the driver names its login from `USER`, which an environment built from nothing does not have: every query in the copy failed. When no `PGUSER` is carried, the child gets the OS user it runs as (the identity peer auth checks anyway). And the dump file, a full copy of the live database, is now deleted as soon as the restore has read it; a service restart had left one in `/tmp` for eleven hours.
|
|
100
101
|
|
|
101
102
|
## Consequences
|
|
102
103
|
|
|
@@ -2785,5 +2785,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2785
2785
|
landed since 1.20.62 with no explicit bump. run 36850426143. (task 1002620)
|
|
2786
2786
|
1.20.64 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2787
2787
|
landed since 1.20.63 with no explicit bump. run 36851876428. (task 1002620)
|
|
2788
|
+
1.20.65 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2789
|
+
landed since 1.20.64 with no explicit bump. run 36852902728. (task 1002620)
|
|
2788
2790
|
---------------------------------------------------------------------------
|
|
2789
2791
|
```
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// is pointed at an empty scratch directory so a config file in the real home (the
|
|
14
14
|
// Discord webhook file among them) is not found.
|
|
15
15
|
|
|
16
|
+
const os = require('node:os');
|
|
17
|
+
|
|
16
18
|
// The only names carried over from the live process.
|
|
17
19
|
// PATH so node, and the child's own tooling, resolve
|
|
18
20
|
// NODE_ENV production behaviour, as the live hall runs it
|
|
@@ -61,9 +63,10 @@ const MODULES_OFF = Object.freeze(PREFIXES.flatMap((pfx) => MODULE_KEYS.map((k)
|
|
|
61
63
|
* @param {string} [o.envPrefix] the env prefix of the copied instance's branding pack, if any
|
|
62
64
|
* @param {string} [o.instanceRoot] the scratch directory shaped like an instance root (config/,
|
|
63
65
|
* migrations/instance/); the child reads its host content from it
|
|
66
|
+
* @param {Function} [o.osUser] () => the OS login name; PGUSER when none is carried
|
|
64
67
|
* @returns {Record<string,string>} a fresh object, never `env` itself
|
|
65
68
|
*/
|
|
66
|
-
function buildPreviewEnv({ env = process.env, port, scratchHome, instanceRoot, envPrefix } = {}) {
|
|
69
|
+
function buildPreviewEnv({ env = process.env, port, scratchHome, instanceRoot, envPrefix, osUser = () => os.userInfo().username } = {}) {
|
|
67
70
|
if (!port) throw new TypeError('buildPreviewEnv: port is required');
|
|
68
71
|
if (!scratchHome) throw new TypeError('buildPreviewEnv: scratchHome is required');
|
|
69
72
|
const out = {};
|
|
@@ -71,6 +74,12 @@ function buildPreviewEnv({ env = process.env, port, scratchHome, instanceRoot, e
|
|
|
71
74
|
if (env[name] !== undefined && env[name] !== '') out[name] = String(env[name]);
|
|
72
75
|
}
|
|
73
76
|
if (!out.NODE_ENV) out.NODE_ENV = 'production';
|
|
77
|
+
// THE LOGIN NAME IS STATED (task 1004388). With no PGUSER the database driver takes it from
|
|
78
|
+
// USER, which an environment built from nothing does not have, and every query fails with
|
|
79
|
+
// "no PostgreSQL user name specified". The OS user is exactly who peer auth checks.
|
|
80
|
+
if (!out.PGUSER) {
|
|
81
|
+
try { const u = osUser(); if (u) out.PGUSER = String(u); } catch { /* left unset: the driver's own default */ }
|
|
82
|
+
}
|
|
74
83
|
out.PORT = String(port);
|
|
75
84
|
out.HOST = '127.0.0.1';
|
|
76
85
|
out.HOME = scratchHome;
|
|
@@ -217,6 +217,10 @@ function createSupervisor({
|
|
|
217
217
|
await step('create-copy', cmds.createDbCommand());
|
|
218
218
|
await step('dump', cmds.dumpCommand({ liveDb, dumpFile }));
|
|
219
219
|
await step('restore', cmds.restoreCommand({ dumpFile }));
|
|
220
|
+
// The dump is a full copy of the live database, sessions included, and is not needed
|
|
221
|
+
// once restored. A restart of the live service cannot finish the stop that would have
|
|
222
|
+
// removed it, so it goes now (task 1004388).
|
|
223
|
+
try { fsImpl.rmSync(dumpFile, { force: true }); } catch { /* the stop's cleanup still removes workDir */ }
|
|
220
224
|
await step('migrate', cmds.migrateCommand({ cacheRoot, version, env: childEnv, instanceDir }));
|
|
221
225
|
|
|
222
226
|
set({ stage: 'launch' });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.65",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.65",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.65",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8513,5 +8513,11 @@
|
|
|
8513
8513
|
"id": "1004390",
|
|
8514
8514
|
"text": "When someone adds a prerequisite to a job that was already waiting in the \"available work\" list, the job used to stay on that list — a builder would pick it up, and only then be told they couldn't start it yet. The listing was"
|
|
8515
8515
|
}
|
|
8516
|
+
],
|
|
8517
|
+
"1.20.65": [
|
|
8518
|
+
{
|
|
8519
|
+
"id": "1004486",
|
|
8520
|
+
"text": "Previewing an older version from the deploy page now works on the real server: the preview could not log in to its copy of the database, so every page in it failed. It also no longer leaves a full copy of the live database lyi"
|
|
8521
|
+
}
|
|
8516
8522
|
]
|
|
8517
8523
|
}
|
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.20.
|
|
78
|
+
const CORE_VERSION = '1.20.65'; // 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');
|
|
@@ -149,7 +149,7 @@ test('a probe that sees a database failure fails the preview, says why, and clea
|
|
|
149
149
|
assert.match(s.error, /schemaPending null/);
|
|
150
150
|
assert.ok(events.includes('kill:SIGTERM'), 'the child was stopped');
|
|
151
151
|
assert.deepEqual(calls[calls.length - 1], cmds.dropDbCommand(), 'the copy was dropped');
|
|
152
|
-
assert.equal(fake.rm.length, 1, 'the scratch directory was removed');
|
|
152
|
+
assert.equal(fake.rm.filter((p) => !String(p).replace(/\\/g, '/').endsWith('/live.dump')).length, 1, 'the scratch directory was removed');
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
test('a preview is running only once the probe says it can serve data', async () => {
|
|
@@ -98,3 +98,26 @@ test('a missing port or scratch home is refused rather than guessed', () => {
|
|
|
98
98
|
assert.throws(() => buildPreviewEnv({ env: LIVE, scratchHome: '/h' }), /port/);
|
|
99
99
|
assert.throws(() => buildPreviewEnv({ env: LIVE, port: 1 }), /scratchHome/);
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
// THE FIRST REAL RUN ON THE DROPLET (task 1004388). The live hall connects by Unix-socket peer
|
|
103
|
+
// auth with no PGUSER set, so the database driver takes the login name from USER — and an
|
|
104
|
+
// environment built from nothing has no USER. Every query in the preview then failed with
|
|
105
|
+
// "no PostgreSQL user name specified", while the migration (psql, which asks the OS) passed.
|
|
106
|
+
// The login name is therefore stated: the carried PGUSER when there is one, else the OS user
|
|
107
|
+
// this process runs as — the same identity peer auth would have checked.
|
|
108
|
+
test('with no PGUSER to carry, the preview logs in as the OS user it runs as', () => {
|
|
109
|
+
const { PGUSER, ...noUser } = LIVE;
|
|
110
|
+
const out = buildPreviewEnv({ env: noUser, port: 3190, scratchHome: '/h', osUser: () => 'lars' });
|
|
111
|
+
assert.equal(out.PGUSER, 'lars');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a carried PGUSER wins over the OS user', () => {
|
|
115
|
+
const out = buildPreviewEnv({ env: LIVE, port: 3190, scratchHome: '/h', osUser: () => 'lars' });
|
|
116
|
+
assert.equal(out.PGUSER, 'hall');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('an OS user that cannot be read leaves PGUSER unset rather than failing the start', () => {
|
|
120
|
+
const { PGUSER, ...noUser } = LIVE;
|
|
121
|
+
const out = buildPreviewEnv({ env: noUser, port: 3190, scratchHome: '/h', osUser: () => { throw new Error('no passwd entry'); } });
|
|
122
|
+
assert.equal('PGUSER' in out, false);
|
|
123
|
+
});
|
|
@@ -46,6 +46,8 @@ function rig({ failOn, probeResults, env } = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const fwd = (x) => String(x).split(String.fromCharCode(92)).join('/');
|
|
49
|
+
// The scratch directory's own removals; the dump file is removed separately, right after the restore.
|
|
50
|
+
const scratchRms = (r) => r.filter((p) => !fwd(p).endsWith('/live.dump'));
|
|
49
51
|
const DROP = { cmd: 'dropdb', args: ['--if-exists', '--force', PREVIEW_DB] };
|
|
50
52
|
|
|
51
53
|
test('a start runs install, copy, restore and migrate, in that order, then launches and waits for health', async () => {
|
|
@@ -92,7 +94,7 @@ for (const failOn of ['npm', 'dropdb', 'createdb', 'dump', 'pg_restore', 'bash']
|
|
|
92
94
|
assert.match(s.error, failOn === 'dropdb' ? /could not be dropped/ : /failed [(]exit 3[)]/);
|
|
93
95
|
assert.equal(fake.spawned.length, 0, 'no process was started');
|
|
94
96
|
if (failOn !== 'dropdb') assert.deepEqual(calls[calls.length - 1], DROP, 'the copy is dropped last');
|
|
95
|
-
assert.equal(rmCalls.length, 1, 'the scratch directory is removed');
|
|
97
|
+
assert.equal(scratchRms(rmCalls).length, 1, 'the scratch directory is removed');
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -102,7 +104,7 @@ test('a preview that never answers is stopped and cleaned up', async () => {
|
|
|
102
104
|
assert.equal(sup.status().state, 'failed');
|
|
103
105
|
assert.match(sup.status().error, /did not answer/);
|
|
104
106
|
assert.equal(fake.killed, 1);
|
|
105
|
-
assert.equal(rmCalls.length, 1);
|
|
107
|
+
assert.equal(scratchRms(rmCalls).length, 1);
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
test('only one preview at a time: a second version is refused, the same version is not restarted', async () => {
|
|
@@ -139,7 +141,7 @@ test('stop kills the child, drops the copy, removes scratch and returns to idle'
|
|
|
139
141
|
await sup.stop();
|
|
140
142
|
assert.equal(fake.killed, 1);
|
|
141
143
|
assert.deepEqual(calls[calls.length - 1], DROP);
|
|
142
|
-
assert.equal(rmCalls.length, 1);
|
|
144
|
+
assert.equal(scratchRms(rmCalls).length, 1);
|
|
143
145
|
assert.equal(sup.status().state, 'idle');
|
|
144
146
|
assert.equal(sup.status().version, null);
|
|
145
147
|
assert.equal(sup.start('1.19.1001').ok, true, 'and a new one may start');
|
|
@@ -201,3 +203,16 @@ test('when the running version is not in the list the window counts from the new
|
|
|
201
203
|
assert.equal(previewable('1.19.1029', { published, running: '0.0.1' }).ok, true);
|
|
202
204
|
assert.equal(previewable('1.19.1028', { published, running: '0.0.1' }).ok, false);
|
|
203
205
|
});
|
|
206
|
+
|
|
207
|
+
// THE DUMP IS A FULL COPY OF THE LIVE DATABASE, sessions included (task 1004388). It is needed
|
|
208
|
+
// only until the restore has read it; leaving it to the stop's cleanup meant a restart of the
|
|
209
|
+
// live service (which cannot finish an async stop) left it in /tmp — found there 11 hours on.
|
|
210
|
+
test('the dump file is deleted as soon as the restore has read it, before the child launches', async () => {
|
|
211
|
+
const { sup, rmCalls, fake } = rig();
|
|
212
|
+
const r = sup.start('1.19.1000');
|
|
213
|
+
await r.done;
|
|
214
|
+
const dumpRm = rmCalls.findIndex((p) => fwd(p).endsWith('/live.dump'));
|
|
215
|
+
assert.ok(dumpRm >= 0, 'the dump was never removed: ' + JSON.stringify(rmCalls));
|
|
216
|
+
assert.equal(sup.status().state, 'running');
|
|
217
|
+
assert.equal(fake.spawned.length, 1);
|
|
218
|
+
});
|