@bongos/core 1.20.63 → 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 +27 -27
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +13 -2
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +1 -0
- package/docs/copy-inventory.md +43 -42
- package/docs/copy-registry.json +62 -53
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/05-versioning.mmd +2 -0
- package/docs/onboarding/diagrams/assertions.json +4 -0
- package/docs/page-readings.json +552 -557
- package/modules/npm-release/preview/env.js +10 -1
- package/modules/npm-release/preview/supervisor.js +4 -0
- package/modules/provisioning/render-standup.js +8 -0
- package/modules/public-landing/public/projects.html +23 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/module-api.js +1 -1
- package/tests/dependency_writes_atomic.mjs +36 -0
- 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/tests/projects_hub_app_step.mjs +34 -1
- package/tests/provisioning_render_route.mjs +3 -0
|
@@ -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' });
|
|
@@ -53,6 +53,13 @@ const SERVICE_RE = /^srv-[a-z0-9]{8,64}$/;
|
|
|
53
53
|
|
|
54
54
|
// The reason a request cannot be queued, in the owner's words, or null. `body` is the
|
|
55
55
|
// validated request body.
|
|
56
|
+
//
|
|
57
|
+
// The hall is never the app (ADR 0345 D1). A command that runs the Bongos core itself puts
|
|
58
|
+
// a second, database-less copy of the project's hall on Render, where it never answers and
|
|
59
|
+
// the deploy times out — measured live on project 43 (task 1004483). It cannot see through
|
|
60
|
+
// the scaffold's own `npm start`, which still runs the hall; that is task 1004484.
|
|
61
|
+
const HALL_COMMAND_RE = /platform-server\.js|node_modules[\\/]@bongos[\\/]core/i;
|
|
62
|
+
const HALL_REFUSAL = 'That command starts the project’s Builders’ hall, which we already host — not your app. Give the command that starts your app.';
|
|
56
63
|
function badRequest(body) {
|
|
57
64
|
const b = body || {};
|
|
58
65
|
if (!KEY_RE.test(String(b.key || ''))) return 'That doesn’t look like a Render API key. It starts with “rnd_” — create one under Account Settings → API Keys in Render.';
|
|
@@ -65,6 +72,7 @@ function badRequest(body) {
|
|
|
65
72
|
if (!RUNTIMES.includes(b.runtime)) return 'Say what your app is built with — for example Node or Python, or Docker to use your repository’s Dockerfile.';
|
|
66
73
|
if (b.runtime === 'docker' && (b.build_command || b.start_command)) return 'Docker builds and starts your app from your Dockerfile, so leave the commands out.';
|
|
67
74
|
if (b.runtime !== 'docker' && !String(b.start_command || '').trim()) return 'Say how your app starts — the command Render should run, for example “npm start”.';
|
|
75
|
+
if (HALL_COMMAND_RE.test(String(b.start_command || '')) || HALL_COMMAND_RE.test(String(b.build_command || ''))) return HALL_REFUSAL;
|
|
68
76
|
}
|
|
69
77
|
if (b.web_plan != null && !WEB_PLANS.includes(b.web_plan)) return `“${b.web_plan}” is not a Render web plan.`;
|
|
70
78
|
if (b.db_plan != null && !DB_PLANS.includes(b.db_plan)) return `“${b.db_plan}” is not a Render database plan.`;
|
|
@@ -1696,6 +1696,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1696
1696
|
<span>Create the project now. You can choose where your app runs afterwards.</span></span>
|
|
1697
1697
|
</label>
|
|
1698
1698
|
</fieldset>
|
|
1699
|
+
<p class="renderWhy" id="renderLater" role="status" hidden>Your new repository has no app in it yet, so there is nothing for Render to run today. Once your builders have written the app, set it up on Render from your project’s page — it takes a minute, and asks for your Render key once. Nothing is created on Render now.</p>
|
|
1699
1700
|
<div class="renderSetup" id="renderSetup" hidden>
|
|
1700
1701
|
<p class="renderWhy">Your Render key is used once, to create your app on your own Render account, and then thrown away — it is not kept, not even in this browser. Render bills you directly. A key can act on your whole Render account, so only paste it here. <a href="https://dashboard.render.com/u/settings#api-keys" target="_blank" rel="noopener noreferrer">Create a key in Render</a></p>
|
|
1701
1702
|
<div class="renderKeyRow">
|
|
@@ -5527,7 +5528,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5527
5528
|
}
|
|
5528
5529
|
function appRow() {
|
|
5529
5530
|
if (appPlatformChoice() !== 'render') return 'Not chosen yet — you can decide later';
|
|
5530
|
-
|
|
5531
|
+
if (renderLater()) return 'Render — set up from your project’s page once your app has code. Nothing is created on Render now.';
|
|
5532
|
+
var ws =esc(state.renderOwnerName || state.renderOwnerId || '');
|
|
5531
5533
|
if (state.renderMode === 'adopt') return 'Render, workspace ' + ws + ' — link <code>' + esc(state.renderServiceName || state.renderServiceId || '') + '</code>, changing nothing on it';
|
|
5532
5534
|
var plan = planLabel(RENDER_WEB_PLANS, state.renderPlan || 'starter');
|
|
5533
5535
|
var db = state.renderDb ? ', with a database (' + planLabel(RENDER_DB_PLANS, state.renderDbPlan || 'basic_256mb') + ')' : ', no database';
|
|
@@ -5543,13 +5545,24 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5543
5545
|
1004184). Here it reads and writes the draft (`state`), so every answer but the key
|
|
5544
5546
|
survives a reload. The key is never on state, so a resumed draft asks for it again,
|
|
5545
5547
|
and says why. */
|
|
5548
|
+
/* A brand-new repository holds only the hall's scaffold — no app — so Render is set
|
|
5549
|
+
up now only for an ADOPTED repo (task 1004483, owner call 2026-10-01). Set up on a
|
|
5550
|
+
fresh one, Render ran the repo's `npm start`, which is the hall, and timed out.
|
|
5551
|
+
Choosing Render for a fresh repo is still an answer: the step says when and where
|
|
5552
|
+
it happens, and nothing is sent to Render at Create. */
|
|
5553
|
+
function renderNow() { return appPlatformChoice() === 'render' && state.mode === 'adopt'; }
|
|
5554
|
+
function renderLater() { return appPlatformChoice() === 'render' && state.mode !== 'adopt'; }
|
|
5546
5555
|
var rc = renderConnect($, state, {
|
|
5547
5556
|
save: saveDraft,
|
|
5548
5557
|
repo: function () { return String(state.owner || '') + '/' + String(state.repo || ''); },
|
|
5549
|
-
on:
|
|
5558
|
+
on: renderNow,
|
|
5550
5559
|
noKey: 'Check your Render key first — or choose “Decide later”.'
|
|
5551
5560
|
});
|
|
5552
|
-
function syncRenderSetup() {
|
|
5561
|
+
function syncRenderSetup() {
|
|
5562
|
+
var later = $('renderLater');
|
|
5563
|
+
if (later) later.hidden = !renderLater();
|
|
5564
|
+
rc.sync();
|
|
5565
|
+
}
|
|
5553
5566
|
function forgetRenderAnswers() { rc.forget(); }
|
|
5554
5567
|
function validateRender() { return rc.validate(); }
|
|
5555
5568
|
function renderRequestBody() { return rc.requestBody(); }
|
|
@@ -5557,7 +5570,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5557
5570
|
// answer. The project exists either way; a refusal here is the APP's, and the done
|
|
5558
5571
|
// panel says so in plain words.
|
|
5559
5572
|
function sendRenderSetup(instanceId) {
|
|
5560
|
-
if (
|
|
5573
|
+
if (renderLater()) {
|
|
5574
|
+
var later = $('doneRender');
|
|
5575
|
+
later.hidden = false; later.className = 'hint';
|
|
5576
|
+
later.textContent = 'Your app goes on Render once it has code: set it up from your project’s page when your builders have written it.';
|
|
5577
|
+
return;
|
|
5578
|
+
}
|
|
5579
|
+
if (!renderNow() || !rc.hasKey() || !instanceId) return;
|
|
5561
5580
|
var body = renderRequestBody();
|
|
5562
5581
|
rc.drop();
|
|
5563
5582
|
var note = $('doneRender');
|
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
|
@@ -8503,5 +8503,21 @@
|
|
|
8503
8503
|
"id": "1004405",
|
|
8504
8504
|
"text": "When the Upgrade button refuses a jump to a new release line, it now tells you exactly which setting on /deploy to change, instead of pointing at a setting that didn't exist."
|
|
8505
8505
|
}
|
|
8506
|
+
],
|
|
8507
|
+
"1.20.64": [
|
|
8508
|
+
{
|
|
8509
|
+
"id": "1004483",
|
|
8510
|
+
"text": "When you start a brand-new project and choose Render for your app, the wizard no longer tries to put anything on Render straight away: a new repository has no app in it yet, and Render was running the project's builders' hall"
|
|
8511
|
+
},
|
|
8512
|
+
{
|
|
8513
|
+
"id": "1004390",
|
|
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
|
+
}
|
|
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
|
+
}
|
|
8506
8522
|
]
|
|
8507
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');
|
|
@@ -188,4 +188,40 @@ await test('routes/tasks.js: POST uses the atomic batch writer and both writers
|
|
|
188
188
|
assert.match(mapper, /CYCLE[\s\S]*409/);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
// ================= the writers never DEMOTE the target (task 1004390) =================
|
|
192
|
+
//
|
|
193
|
+
// The decision recorded in ADR 0015 ("A dependency added AFTER promotion"): an edge
|
|
194
|
+
// written against a task that is already `ready` leaves the status alone, and the
|
|
195
|
+
// CLAIMABLE FEED hides the row instead (core_264 / task 1004190). The branch that was
|
|
196
|
+
// rejected is the cheap one — demote the target to `backlog` here, no migration needed —
|
|
197
|
+
// so it is the one that will look like a tidy-up to a future reader. These tests make
|
|
198
|
+
// re-adding it go red: the writers must issue no write against `tasks` at all, which is
|
|
199
|
+
// what keeps a planner's edge from silently reversing a human's `POST /promote`.
|
|
200
|
+
//
|
|
201
|
+
// Narrow on purpose. `SELECT id FROM tasks` (the existence check) is legitimate and
|
|
202
|
+
// stays; what is forbidden is a MUTATION of the row.
|
|
203
|
+
const touchesTaskRows = (log) => log.filter((q) => /^(UPDATE|DELETE FROM|INSERT INTO) tasks\b/i.test(q.sql));
|
|
204
|
+
|
|
205
|
+
await test('addTaskDependencies writes edges only — it never updates the task it blocks', async () => {
|
|
206
|
+
const p = fakePool({ existing: [1, 2] });
|
|
207
|
+
await addTaskDependencies(1, [2], { pool: p });
|
|
208
|
+
assert.deepEqual(touchesTaskRows(p.log), [],
|
|
209
|
+
'a dep edge must not demote its target — ADR 0015, the rejected branch');
|
|
210
|
+
assert.ok(!has(p.log, /'backlog'/), 'and nothing here names the backlog status');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await test('setTaskDependencies (the full-set replace) never updates the task either', async () => {
|
|
214
|
+
const p = fakePool({ existing: [1, 2] });
|
|
215
|
+
await setTaskDependencies(1, [2], { pool: p });
|
|
216
|
+
assert.deepEqual(touchesTaskRows(p.log), []);
|
|
217
|
+
// The one DELETE it does run is scoped to the edge table, not the task row.
|
|
218
|
+
assert.ok(has(p.log, /^DELETE FROM dependencies/));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await test('addDependency (the generic edge) never updates the from-node', async () => {
|
|
222
|
+
const p = fakePool();
|
|
223
|
+
await addDependency('task', 1, 'task', 2, { pool: p });
|
|
224
|
+
assert.deepEqual(touchesTaskRows(p.log), []);
|
|
225
|
+
});
|
|
226
|
+
|
|
191
227
|
summary();
|
|
@@ -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
|
+
});
|
|
@@ -99,7 +99,9 @@ function load({ draft = {}, answers = {} } = {}) {
|
|
|
99
99
|
},
|
|
100
100
|
querySelectorAll(sel) { assert.equal(sel, '#appOpts input[name=appPlatform]'); return inputs; },
|
|
101
101
|
};
|
|
102
|
-
|
|
102
|
+
// mode 'adopt' by default: since task 1004483 the Render flow runs at creation only for an
|
|
103
|
+
// adopted repo — a fresh one has no app yet. The fresh-repo cases set mode themselves.
|
|
104
|
+
const state = Object.assign({ appPlatform: 'later', owner: 'example-owner', repo: 'spike', mode: 'adopt' }, draft);
|
|
103
105
|
const fetch = async (url, init) => {
|
|
104
106
|
const body = init && init.body ? JSON.parse(init.body) : null;
|
|
105
107
|
calls.push({ url, method: init && init.method, body });
|
|
@@ -417,6 +419,37 @@ test('after Create the key is sent once, to the new project, and then dropped',
|
|
|
417
419
|
assert.equal(w.calls.filter((c) => c.url.endsWith('/render')).length, 1, 'a second send has nothing to send');
|
|
418
420
|
});
|
|
419
421
|
|
|
422
|
+
// task 1004483 — live on project 43: a fresh repo holds only the hall's scaffold, so Render
|
|
423
|
+
// ran the hall and timed out. A fresh repo records the choice and creates nothing on Render.
|
|
424
|
+
test('a brand-new repo choosing Render asks for no key, says when it happens, and sends nothing at Create', async () => {
|
|
425
|
+
const w = load({ answers: ANSWERS, draft: { appPlatform: 'render', mode: 'greenfield' } });
|
|
426
|
+
w.syncRenderSetup();
|
|
427
|
+
assert.equal(w.$('renderLater').hidden, false, 'the note shows');
|
|
428
|
+
assert.equal(w.$('renderSetup').hidden, true, 'no key, workspace or plan questions');
|
|
429
|
+
assert.equal(w.validateRender(), true, 'Next is not blocked on a key nobody needs yet');
|
|
430
|
+
assert.match(w.appRow(), /^Render — set up from your project’s page once your app has code\. Nothing is created on Render now\.$/);
|
|
431
|
+
w.sendRenderSetup(42);
|
|
432
|
+
await flush();
|
|
433
|
+
assert.equal(w.calls.filter((c) => c.url.endsWith('/render')).length, 0, 'nothing is sent to Render');
|
|
434
|
+
assert.equal(w.$('doneRender').hidden, false);
|
|
435
|
+
assert.match(w.$('doneRender').textContent, /once it has code: set it up from your project’s page/);
|
|
436
|
+
assert.equal(w.watches.length, 0, 'there is no app progress to watch');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('switching the same draft to an adopted repo brings the full Render flow back', () => {
|
|
440
|
+
const w = load({ draft: { appPlatform: 'render', mode: 'greenfield' } });
|
|
441
|
+
w.syncRenderSetup();
|
|
442
|
+
assert.equal(w.$('renderLater').hidden, false);
|
|
443
|
+
w.state.mode = 'adopt';
|
|
444
|
+
w.syncRenderSetup();
|
|
445
|
+
assert.equal(w.$('renderLater').hidden, true);
|
|
446
|
+
assert.equal(w.$('renderSetup').hidden, false);
|
|
447
|
+
assert.equal(w.validateRender(), false, 'an adopted repo needs the key checked');
|
|
448
|
+
w.state.appPlatform = 'later';
|
|
449
|
+
w.syncRenderSetup();
|
|
450
|
+
assert.equal(w.$('renderLater').hidden, true, 'Decide later shows neither');
|
|
451
|
+
});
|
|
452
|
+
|
|
420
453
|
test('a refused setup request says the project is still being created', async () => {
|
|
421
454
|
const w = load({ answers: { ...ANSWERS, '/api/bongos/provisioning/instances/42/render': { status: 409, message: 'This project has no GitHub repository linked.' } }, draft: { appPlatform: 'render' } });
|
|
422
455
|
w.typeKey(KEY);
|
|
@@ -136,6 +136,9 @@ for (const [name, body, re] of [
|
|
|
136
136
|
['an unknown runtime', { ...GREEN, runtime: 'cobol' }, /built with/],
|
|
137
137
|
['a native runtime with no start command', { ...GREEN, start_command: ' ' }, /how your app starts/],
|
|
138
138
|
['docker with commands', { ...GREEN, runtime: 'docker' }, /leave the commands out/],
|
|
139
|
+
// task 1004483 — the hall is never the app; on Render it has no database and times out
|
|
140
|
+
['a start command that runs the hall', { ...GREEN, start_command: 'node node_modules/@bongos/core/src/platform-server.js' }, /starts the project’s Builders’ hall/],
|
|
141
|
+
['the hall by its file alone', { ...GREEN, start_command: 'node platform-server.js' }, /starts the project’s Builders’ hall/],
|
|
139
142
|
['adopt saying how to build', { key: KEY, owner_id: 'tea-abcdefgh1234', mode: 'adopt', service_id: 'srv-abcdefgh1234', runtime: 'node' }, /exactly as it is/],
|
|
140
143
|
]) {
|
|
141
144
|
await ta(`${name} → 400, nothing kept`, async () => {
|