@bongos/core 1.21.69 → 1.21.71
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 +119 -64
- package/.claude/skills/bongos-feedback/SKILL.md +10 -46
- package/.claude/skills/goal-create/SKILL.md +21 -3
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -2
- package/clients/bongos-client/index.cjs +8 -2
- package/clients/bongos-client/index.d.ts +15 -6
- package/clients/bongos-client/index.mjs +8 -2
- package/docs/adr/0368-red-and-blue-teams-practice-on-a-range-never-on-live.md +99 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +196 -19
- package/docs/api-reference.md +9 -6
- package/docs/copy-inventory.md +25 -21
- package/docs/copy-registry.json +76 -30
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +8 -8
- package/docs/recipes/core-release-pipeline.md +6 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/modules/hall-ui/public/settings-software-update.js +77 -2
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/lifecycle/db-tasks.js +12 -0
- package/modules/lifecycle/goal-spec-task.js +129 -0
- package/modules/lifecycle/routes/goals.js +7 -9
- package/modules/lifecycle/routes/task-write-routes.js +11 -1
- package/modules/lifecycle/upkeep-home.js +55 -0
- package/modules/platform-identity/instance-feedback.js +108 -7
- package/modules/platform-identity/routes/sso.js +71 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +42 -0
- package/scripts/gds/claude-materialize.js +125 -31
- package/scripts/gds/claude-md-template.js +47 -10
- package/scripts/gds/cli-lib.js +4 -4
- package/scripts/gds/feedback-send.js +146 -73
- package/scripts/gds/paste-token.js +1 -1
- package/scripts/gds/provision-net.js +1 -8
- package/scripts/gds/provision-units.js +28 -2
- package/scripts/gds/reauth.js +1 -1
- package/scripts/gds/run-unit-tests.js +3 -0
- package/scripts/gds/setup.js +1 -1
- package/scripts/gds/start.js +4 -4
- package/scripts/gds/surface.js +15 -3
- package/scripts/gds/task.js +32 -5
- package/scripts/gds/upgrade-operator.js +73 -0
- package/scripts/gds/upgrade.js +7 -2
- package/scripts/hall-preview/server.js +3 -0
- package/src/bongos/platform-visibility-gate.js +3 -0
- package/src/bongos/routes/core-update.js +98 -4
- package/src/bongos/routes/feedback.js +100 -76
- package/src/bongos/upgrade-request.js +158 -0
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +195 -18
- package/tests/cli_allowlist_permissions.mjs +6 -2
- package/tests/cli_surface.mjs +2 -2
- package/tests/fixtures/page-ledger.mjs +5 -0
- package/tests/goal_admission_override.mjs +1 -1
- package/tests/goal_category.mjs +1 -1
- package/tests/goal_create_spec_entry.mjs +61 -0
- package/tests/goal_spec_task.mjs +197 -0
- package/tests/goal_spec_task_db.mjs +95 -0
- package/tests/instance_feedback.mjs +267 -68
- package/tests/provision_settings_apply.mjs +25 -1
- package/tests/software_update.mjs +18 -1
- package/tests/start_env_guard.mjs +2 -2
- package/tests/upgrade_operator.mjs +105 -0
- package/tests/upgrade_request.mjs +348 -0
- package/tests/upkeep_home.mjs +156 -0
|
@@ -86,7 +86,7 @@ const API_BASE = RESOLVED.base;
|
|
|
86
86
|
function usage(extra) {
|
|
87
87
|
if (extra) console.error(extra);
|
|
88
88
|
console.error('');
|
|
89
|
-
console.error('Usage: bongos paste-token <token> [--api-base <url>]');
|
|
89
|
+
console.error('Usage: npx bongos paste-token <token> [--api-base <url>]');
|
|
90
90
|
console.error(' (inside a core checkout: node scripts/gds/paste-token.js <token>)');
|
|
91
91
|
console.error('');
|
|
92
92
|
// Fall back to the inferred base for this hint when --api-base was the thing that
|
|
@@ -12,14 +12,7 @@
|
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
const crypto = require('node:crypto');
|
|
14
14
|
const { CONFIG } = require('./provision-config.js');
|
|
15
|
-
const { provisionedEnvPrefix } = require('./provision-units.js');
|
|
16
|
-
|
|
17
|
-
function federationHubOrigin() {
|
|
18
|
-
const explicit = process.env.PROVISION_HUB_ORIGIN;
|
|
19
|
-
if (explicit) return String(explicit).replace(/\/+$/, '');
|
|
20
|
-
const own = process.env[`${provisionedEnvPrefix()}_PUBLIC_ORIGIN`];
|
|
21
|
-
return own ? String(own).replace(/\/+$/, '') : null;
|
|
22
|
-
}
|
|
15
|
+
const { federationHubOrigin, provisionedEnvPrefix } = require('./provision-units.js'); // lives beside the hub-upgrade address it also feeds (task 1004608)
|
|
23
16
|
|
|
24
17
|
// Whether provisioning should federate this instance's sign-in through the hub.
|
|
25
18
|
//
|
|
@@ -258,13 +258,15 @@ GITHUB_APP_PRIVATE_KEY=${String(appPem).replace(/\r?\n/g, '\\n')}
|
|
|
258
258
|
// Postgres with every sibling and the control plane, so its pool may not take pg's
|
|
259
259
|
// default 10 of the server's 97. src/bongos/pool.js reads it bare.
|
|
260
260
|
const poolLine = `PGPOOL_MAX=${CONFIG.instancePoolMax}\n`;
|
|
261
|
+
// Where this project's Update button goes (task 1004608) — see hubUpgradeEnvVarsFor.
|
|
262
|
+
const hubUpgradeLines = Object.entries(hubUpgradeEnvVarsFor(inst)).map(([k, v]) => `${k}=${v}\n`).join('');
|
|
261
263
|
return `# /etc/${inst.slug}/web.env — per-instance secrets (chmod 600, OUTSIDE the repo).
|
|
262
264
|
${credComment}
|
|
263
265
|
${prefix}_GITHUB_CLIENT_ID=${clientId || ''}
|
|
264
266
|
${prefix}_GITHUB_CLIENT_SECRET=${clientSecret || ''}
|
|
265
267
|
${firstAdminLine}${originLines}${appLines}PORT=${inst.port}
|
|
266
268
|
PGDATABASE=${dbName(inst)}
|
|
267
|
-
${poolLine}${dbLines}${fedLines}${settingsEnvLines(inst, prefix)}`;
|
|
269
|
+
${poolLine}${dbLines}${fedLines}${hubUpgradeLines}${settingsEnvLines(inst, prefix)}`;
|
|
268
270
|
}
|
|
269
271
|
function webEnvPath(inst, { etcBase = CONFIG.etcBase } = {}) {
|
|
270
272
|
return path.posix.join(etcBase, inst.slug, 'web.env');
|
|
@@ -295,7 +297,7 @@ function originEnvVarsFor(inst) {
|
|
|
295
297
|
function settingsEnvVarsFor(inst, envBody) {
|
|
296
298
|
const { settingsEnvVars } = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning')); // eslint-disable-line global-require
|
|
297
299
|
const prefix = inst && inst.hosting_shape === 'control-plane' ? selfEnvPrefix({ envBody }) : provisionedEnvPrefix();
|
|
298
|
-
return settingsEnvVars(inst, prefix);
|
|
300
|
+
return { ...settingsEnvVars(inst, prefix), ...hubUpgradeEnvVarsFor(inst) };
|
|
299
301
|
}
|
|
300
302
|
|
|
301
303
|
// The env-var prefix the CONTROL PLANE's own web process resolves (task 1004334). The
|
|
@@ -414,8 +416,32 @@ function renamedEnvCarry(body) {
|
|
|
414
416
|
// <PREFIX>_HUB_ORIGIN (what auth.js loadIdpConfig reads). Explicit PROVISION_HUB_ORIGIN
|
|
415
417
|
// wins; otherwise the control plane's OWN public origin (<PREFIX>_PUBLIC_ORIGIN, the
|
|
416
418
|
// env the hub already runs under). Null → not hub-configured → shouldFederate is false.
|
|
419
|
+
function federationHubOrigin() {
|
|
420
|
+
const explicit = process.env.PROVISION_HUB_ORIGIN;
|
|
421
|
+
if (explicit) return String(explicit).replace(/\/+$/, '');
|
|
422
|
+
const own = process.env[`${provisionedEnvPrefix()}_PUBLIC_ORIGIN`];
|
|
423
|
+
return own ? String(own).replace(/\/+$/, '') : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// WHERE A HOSTED PROJECT'S UPDATE BUTTON GOES (task 1004608): the hub's deploy door for
|
|
427
|
+
// THIS project, written as <PREFIX>_HUB_UPGRADE_URL. The hall used to learn the hub only
|
|
428
|
+
// from the sign-in federation block, which only a co-tenant gets, so every cloud-host
|
|
429
|
+
// project (and any that keeps its own GitHub app) told its owner to run `bongos upgrade`
|
|
430
|
+
// on a server the platform runs. The door admits the owner of both tenant shapes
|
|
431
|
+
// (modules/provisioning/core-upgrade-authz.js), so both get the address. Not a secret and
|
|
432
|
+
// grants nothing: the door re-checks every request. Empty for the tenant shapes when no hub
|
|
433
|
+
// origin resolves (a key that came and went would leave a stale line behind); absent for
|
|
434
|
+
// every other shape, which has no door on this hub.
|
|
435
|
+
const HUB_UPGRADE_SHAPES = new Set(['co-tenant', 'cloud-host']);
|
|
436
|
+
function hubUpgradeEnvVarsFor(inst) {
|
|
437
|
+
if (!inst || !HUB_UPGRADE_SHAPES.has(inst.hosting_shape) || !inst.slug) return {};
|
|
438
|
+
const origin = federationHubOrigin();
|
|
439
|
+
return { [`${provisionedEnvPrefix()}_HUB_UPGRADE_URL`]: origin ? `${origin}/deploy?project=${encodeURIComponent(String(inst.slug))}` : '' };
|
|
440
|
+
}
|
|
417
441
|
|
|
418
442
|
module.exports = {
|
|
443
|
+
federationHubOrigin,
|
|
444
|
+
hubUpgradeEnvVarsFor,
|
|
419
445
|
ENV_PREFIX_PACK,
|
|
420
446
|
envPrefixMismatch,
|
|
421
447
|
normalizePackEnvPrefix,
|
package/scripts/gds/reauth.js
CHANGED
|
@@ -96,7 +96,7 @@ function printGuidance({ opened }) {
|
|
|
96
96
|
console.log(" you're already signed in there via your browser cookie)");
|
|
97
97
|
console.log(' 2. Under the "CLI Access" section, click "Re-issue CLI token".');
|
|
98
98
|
console.log(' 3. Click "Copy" — it copies a one-line command that looks like:');
|
|
99
|
-
console.log(' bongos paste-token <token>');
|
|
99
|
+
console.log(' npx bongos paste-token <token>');
|
|
100
100
|
console.log(' 4. Paste that one line back to Claude. It will run it for you,');
|
|
101
101
|
console.log(' verify the token, and write your new session.');
|
|
102
102
|
console.log('');
|
|
@@ -106,6 +106,9 @@ const INTEGRATION = new Set([
|
|
|
106
106
|
// never was a dedicated consumer-layout-boot workflow, whatever older notes say.
|
|
107
107
|
'consumer_layout_boot',
|
|
108
108
|
'claim_gate_open_blocker', 'claim_gate_rebase',
|
|
109
|
+
// task 1004585: a hall-made goal's spec task gates its other work — the edge, the
|
|
110
|
+
// claim gate's unsatisfiedDeps read, and the ship-time auto-promotion, on real Postgres.
|
|
111
|
+
'goal_spec_task_db',
|
|
109
112
|
// task 1003457: proves the blocker-resolve auto-promotion trigger (migration
|
|
110
113
|
// 009, tightened by core_223) fires on the HALL'S OWN write path — the real
|
|
111
114
|
// buildGdsRouter() HTTP routes, not a static SQL-text pin (blocker_resolve_guard.mjs,
|
package/scripts/gds/setup.js
CHANGED
|
@@ -356,7 +356,7 @@ async function finishSetup(builder) {
|
|
|
356
356
|
console.log(` 3. Claim + ship: ${surfaceAction('claim', 'N')} → ${surfaceAction('ship')}`);
|
|
357
357
|
// Claiming writes code, so it needs a checkout. In a terminal the public CLI answers
|
|
358
358
|
// `bongos claim` with where to get one; say so here rather than let step 3 look self-serve.
|
|
359
|
-
if (isTerminalSurface()) console.log(' (claim + ship run in a checkout — `bongos claim` shows how to get one)');
|
|
359
|
+
if (isTerminalSurface()) console.log(' (claim + ship run in a checkout — `npx bongos claim` shows how to get one)');
|
|
360
360
|
console.log(' 4. Ship three works and you rise to Thetes — the whole queue opens.');
|
|
361
361
|
}
|
|
362
362
|
} catch (_) {
|
package/scripts/gds/start.js
CHANGED
|
@@ -615,7 +615,7 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
|
|
|
615
615
|
out.push(`2. If that doesn't load either, the instance itself is down: \`curl -sI ${origin}/healthz\` says whether it's answering.`);
|
|
616
616
|
out.push(`3. Once it answers, re-run ${surface.code(surface.action('start'))}.`);
|
|
617
617
|
out.push('');
|
|
618
|
-
out.push(`${surface.strong(`Don't run ${surface.code('
|
|
618
|
+
out.push(`${surface.strong(`Don't run ${surface.code(surface.cli('dev'))} for this.`)} It stands up a separate local instance with its own empty database — it cannot show you this board, and it would hide the outage behind an empty one.`);
|
|
619
619
|
if (depsMissing) {
|
|
620
620
|
out.push('');
|
|
621
621
|
out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is ${surface.strong('public')} on npm, so ${surface.code('npm install @bongos/core')} needs no token or login; a failure here is a real error, not the expected tokenless 404 it used to be.)`);
|
|
@@ -629,10 +629,10 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
|
|
|
629
629
|
if (depsMissing) {
|
|
630
630
|
out.push(`${n++}. \`npm install\` — installs the toolchain first (the \`bongos\` CLI ships inside \`@bongos/core\`, so nothing else runs until this does).`);
|
|
631
631
|
}
|
|
632
|
-
out.push(`${n++}.
|
|
632
|
+
out.push(`${n++}. ${surface.code(surface.cli('dev'))} — one command: starts Postgres, builds the tables, runs the server, and signs you in.`);
|
|
633
633
|
out.push(`${n++}. Re-run ${surface.code(surface.action('start'))}.`);
|
|
634
634
|
out.push('');
|
|
635
|
-
out.push(`Meant to work on a *different* instance that's already running? Sign in to it with
|
|
635
|
+
out.push(`Meant to work on a *different* instance that's already running? Sign in to it with ${surface.code(surface.cli('login <url>'))} first.`);
|
|
636
636
|
return out.join('\n');
|
|
637
637
|
}
|
|
638
638
|
function emitInstanceNotRunningCard(opts) {
|
|
@@ -980,7 +980,7 @@ async function main() {
|
|
|
980
980
|
|
|
981
981
|
// ---------- next ----------
|
|
982
982
|
console.log('');
|
|
983
|
-
console.log(`→ ${surface.isTerminal() ? `Claim one with ${surface.code('
|
|
983
|
+
console.log(`→ ${surface.isTerminal() ? `Claim one with ${surface.code(surface.cli('claim <id>'))} in a checkout` : `${surface.strong('Tell me a number')} to claim it`}.`);
|
|
984
984
|
if (full) {
|
|
985
985
|
console.log('');
|
|
986
986
|
console.log('Commands:');
|
package/scripts/gds/surface.js
CHANGED
|
@@ -43,11 +43,23 @@ function taskRef(id, url, env = process.env) {
|
|
|
43
43
|
return isTerminal(env) ? `#${id}` : `[#${id}](${url}${id})`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// How a reader runs the CLI (task 1004621). The `bongos` bin ships inside @bongos/core and is not
|
|
47
|
+
// on PATH — in a project it sits at node_modules/.bin/bongos, and the global @cloudbongos/cli that
|
|
48
|
+
// once put a bare `bongos` on PATH is retired (ADR 0324). `npx` finds the local bin with no setup,
|
|
49
|
+
// in a project and in a core checkout alike, so every printed command uses it. A bare `bongos …`
|
|
50
|
+
// sent the first command of a project's session into `command not found`.
|
|
51
|
+
const CLI = 'npx bongos';
|
|
52
|
+
|
|
53
|
+
// A CLI command line to print: `cli('claim <id>')` → `npx bongos claim <id>`.
|
|
54
|
+
function cli(rest) {
|
|
55
|
+
return rest ? `${CLI} ${rest}` : CLI;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
// An action the reader can actually run. In a session that is the slash command; in a terminal it
|
|
47
|
-
// is the `bongos` verb. `/builder-ship 123` does not exist outside Claude Code.
|
|
59
|
+
// is the `npx bongos` verb. `/builder-ship 123` does not exist outside Claude Code.
|
|
48
60
|
function action(verb, arg, env = process.env) {
|
|
49
61
|
const tail = arg == null ? '' : ` ${arg}`;
|
|
50
|
-
return isTerminal(env) ?
|
|
62
|
+
return isTerminal(env) ? cli(`${verb}${tail}`) : `/builder-${verb}${tail}`;
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
// Markdown emphasis is decoration in a feed and literal asterisks on a console.
|
|
@@ -86,4 +98,4 @@ function table(headers, rows, env = process.env) {
|
|
|
86
98
|
].join('\n');
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
module.exports = { AGENT, TERMINAL, resolveSurface, isTerminal, taskRef, action, strong, em, code, table };
|
|
101
|
+
module.exports = { AGENT, TERMINAL, CLI, cli, resolveSurface, isTerminal, taskRef, action, strong, em, code, table };
|
package/scripts/gds/task.js
CHANGED
|
@@ -19,10 +19,12 @@
|
|
|
19
19
|
// Task CREATION is Archon-gated SERVER-SIDE (creating work shapes scope, ADR 0016). This
|
|
20
20
|
// CLI adds no authority — a lower rank gets a clear 403 message pointing at idea capture.
|
|
21
21
|
|
|
22
|
-
const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
|
|
22
|
+
const { cliClient, requireSession, arg, hasFlag, errCode } = require('./cli-lib');
|
|
23
23
|
// task 1003109: the ship-time VISUAL. The CLI screens the file with the SERVER's
|
|
24
24
|
// own constants, so what it accepts is exactly what the upload route accepts.
|
|
25
25
|
const taskVisuals = require('../../modules/lifecycle/task-visuals');
|
|
26
|
+
// task 1004597: the one spelling of "this is platform upkeep" (kind infra, no goal).
|
|
27
|
+
const { UPKEEP_KIND } = require('../../modules/lifecycle/upkeep-home');
|
|
26
28
|
|
|
27
29
|
// Valid enums (mirrors the server's task-classifier vocab; a friendly client-side check
|
|
28
30
|
// so an obvious typo fails before the round-trip — the server stays authoritative).
|
|
@@ -87,6 +89,8 @@ function usage() {
|
|
|
87
89
|
console.error(' bongos task visual <id> <image> [--alt "caption"] attach or replace the ship-time visual');
|
|
88
90
|
console.error(' bongos task visual <id> --remove remove it again');
|
|
89
91
|
console.error(' bongos task visual <id> <image> --slot <name> attach or replace a named visual (e.g. after-phone-dark)');
|
|
92
|
+
console.error('\n Platform upkeep (a Bongos upgrade, maintenance) needs no goal: pass --kind infra');
|
|
93
|
+
console.error(" and leave out --goal, and it is filed under the version's maintenance goal.");
|
|
90
94
|
console.error(`\n kinds: ${VALID_KINDS.join(' | ')}`);
|
|
91
95
|
console.error(` disciplines: ${VALID_DISCIPLINES.join(' | ')}`);
|
|
92
96
|
}
|
|
@@ -111,6 +115,22 @@ async function resolveDefaultVersion(api) {
|
|
|
111
115
|
return building[0].id;
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
// goalRequiredHelp — PURE: what a builder reads when a task names no goal
|
|
119
|
+
// (task 1004597). Two ways forward: name a product goal, or — when the work is
|
|
120
|
+
// platform upkeep such as a Bongos upgrade — file it as kind infra with no goal.
|
|
121
|
+
function goalRequiredHelp(body) {
|
|
122
|
+
const lines = ['Every task belongs to a goal, and this one did not name one.'];
|
|
123
|
+
if (body && body.kind === UPKEEP_KIND) {
|
|
124
|
+
lines.push(`Version ${body.version_id} is not being built right now, so it has no maintenance goal.`);
|
|
125
|
+
lines.push('Re-run with --version <the version being built> (see `bongos status`).');
|
|
126
|
+
} else {
|
|
127
|
+
lines.push(' - Product work: re-run with --goal <id> (`bongos status` lists the goals).');
|
|
128
|
+
lines.push(' - Platform upkeep (a Bongos upgrade or other maintenance): re-run with --kind infra and no --goal.');
|
|
129
|
+
lines.push(" It goes into this version's maintenance goal.");
|
|
130
|
+
}
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
async function cmdNew(args) {
|
|
115
135
|
const { body, errors } = buildTaskBody(args);
|
|
116
136
|
if (errors.length) { console.error('task new: ' + errors.join('; ') + '\n'); usage(); process.exit(2); }
|
|
@@ -120,7 +140,7 @@ async function cmdNew(args) {
|
|
|
120
140
|
if (!body.version_id) {
|
|
121
141
|
body.version_id = await resolveDefaultVersion(api);
|
|
122
142
|
if (!body.version_id) {
|
|
123
|
-
console.error('task new: could not resolve an active version — pass --version <id> (see `bongos status`).');
|
|
143
|
+
console.error('task new: could not resolve an active version — pass --version <id> (see `npx bongos status`).');
|
|
124
144
|
process.exit(2);
|
|
125
145
|
}
|
|
126
146
|
}
|
|
@@ -128,7 +148,13 @@ async function cmdNew(args) {
|
|
|
128
148
|
if (r.status === 403) {
|
|
129
149
|
console.error('Creating a task is Archon-gated (creating work shapes scope). Your rank is lower, so the server declined.');
|
|
130
150
|
console.error('To propose it instead, capture an idea for triage:');
|
|
131
|
-
console.error(' bongos exec scripts/gds/capture.js "<your idea>"');
|
|
151
|
+
console.error(' npx bongos exec scripts/gds/capture.js "<your idea>"');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
// task 1004597: a goal-less filing is refused unless it is platform upkeep.
|
|
155
|
+
// Say both ways forward in plain words instead of dumping the raw error.
|
|
156
|
+
if (r.status === 400 && errCode(r.data) === 'goal_id_required') {
|
|
157
|
+
console.error(goalRequiredHelp(body));
|
|
132
158
|
process.exit(1);
|
|
133
159
|
}
|
|
134
160
|
if (r.status !== 201 || !r.data?.task) {
|
|
@@ -141,8 +167,9 @@ async function cmdNew(args) {
|
|
|
141
167
|
console.log(`✓ Created task #${task.id} [${task.status}] — ${task.title}`);
|
|
142
168
|
console.log(` kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}`);
|
|
143
169
|
console.log(` ${hallLink(API_BASE, task.id)}`);
|
|
170
|
+
if (r.data.goal_home === 'maintenance') console.log(` Filed under ${body.version_id}'s maintenance goal, the standing home for platform upkeep.`);
|
|
144
171
|
if (task.status !== 'ready') console.log(` (status '${task.status}' — pass --ready to make it claimable now, deps permitting)`);
|
|
145
|
-
console.log(` next: bongos claim ${task.id}`);
|
|
172
|
+
console.log(` next: npx bongos claim ${task.id}`);
|
|
146
173
|
}
|
|
147
174
|
|
|
148
175
|
async function cmdShow(args) {
|
|
@@ -243,4 +270,4 @@ if (require.main === module) {
|
|
|
243
270
|
main().catch((err) => { console.error('fatal:', err); process.exit(1); });
|
|
244
271
|
}
|
|
245
272
|
|
|
246
|
-
module.exports = { buildTaskBody, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink };
|
|
273
|
+
module.exports = { buildTaskBody, goalRequiredHelp, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// scripts/gds/upgrade-operator.js — "who upgrades this project?" for `bongos upgrade`
|
|
2
|
+
// run from a builder's own computer (task 1004596).
|
|
3
|
+
//
|
|
4
|
+
// A hosted project (its Settings → Software update button leads to the hub — door kind
|
|
5
|
+
// 'hub', see src/bongos/routes/core-update.js updateDoor) lives on its operator's server.
|
|
6
|
+
// Its builders work from a laptop checkout that has no database and no server access. Run
|
|
7
|
+
// there, `bongos upgrade` used to say only "cannot determine which database to migrate.
|
|
8
|
+
// Set PGDATABASE…" — and an assistant reading that, plus the release recipe, wrote the
|
|
9
|
+
// builder server steps full of guessed names for a machine they cannot reach. This module
|
|
10
|
+
// replaces that dead end with the one supported path: the operator upgrades it, and here
|
|
11
|
+
// is how to ask.
|
|
12
|
+
//
|
|
13
|
+
// THE CONDITION, and why it cannot touch the real operator path: BOTH must hold —
|
|
14
|
+
// 1. no database is known in this checkout (no PGDATABASE, no db.database in the
|
|
15
|
+
// branding pack). On the server a database is always known, or upgrade.js already
|
|
16
|
+
// refuses there; so the server never reaches step 2.
|
|
17
|
+
// 2. the project, asked through this builder's own CLI session, says its update door is
|
|
18
|
+
// the hub's. No session, no answer, an older core, a self-hosted or provisioning
|
|
19
|
+
// instance → no message, and upgrade.js behaves exactly as before.
|
|
20
|
+
|
|
21
|
+
// Ask the project (via this builder's CLI session) where its Update button leads.
|
|
22
|
+
// Returns { kind, url, hall } or null on ANY failure — a missing answer is never a verdict.
|
|
23
|
+
async function readUpdateDoor({ timeoutMs = 5000 } = {}) {
|
|
24
|
+
try {
|
|
25
|
+
const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT, hallBase } = require('./cli-lib');
|
|
26
|
+
const session = readSessionToken();
|
|
27
|
+
if (!session) return null;
|
|
28
|
+
const res = await dispatchedFetch(`${session.apiBase}/api/bongos/software-update`, {
|
|
29
|
+
headers: { Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT },
|
|
30
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
31
|
+
dispatcher: noKeepAliveDispatcher(),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) return null;
|
|
34
|
+
const body = await res.json();
|
|
35
|
+
const door = body && body.door;
|
|
36
|
+
if (!door || typeof door.kind !== 'string') return null;
|
|
37
|
+
return { kind: door.kind, url: door.url || null, hall: hallBase(session.apiBase) };
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The door when the operator runs this project and this checkout is not where it runs;
|
|
42
|
+
// null otherwise (including whenever the answer cannot be read).
|
|
43
|
+
async function operatorRunsThisProject({ instanceDir }, deps = {}) {
|
|
44
|
+
const { db } = (deps.resolveInstanceDb || require('./upgrade').resolveInstanceDb)({ instanceDir }, deps.run);
|
|
45
|
+
if (db) return null;
|
|
46
|
+
const door = await (deps.readUpdateDoor || readUpdateDoor)();
|
|
47
|
+
return door && door.kind === 'hub' ? door : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// What the builder (and any assistant reading over their shoulder) is told. Plain words,
|
|
51
|
+
// one supported path, no server steps and nothing for anyone to fill in.
|
|
52
|
+
function operatorRunMessage(door = {}) {
|
|
53
|
+
const settings = door.hall ? `${door.hall}/settings#software-update` : 'Settings → Software update';
|
|
54
|
+
return [
|
|
55
|
+
'This project is upgraded by its operator, not from this computer.',
|
|
56
|
+
'It runs on the operator\'s server, and nothing you run here can upgrade it.',
|
|
57
|
+
'',
|
|
58
|
+
'To ask for the upgrade:',
|
|
59
|
+
` • Open ${settings} and use the button there.`,
|
|
60
|
+
' • Or, from this terminal: bongos feedback "Please upgrade this project to the latest version"',
|
|
61
|
+
'',
|
|
62
|
+
'There are no server steps for you to take. An assistant should not write any, or guess',
|
|
63
|
+
'server, database or service names for this project — pass the request on instead.',
|
|
64
|
+
].join('\n') + '\n';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The message to show, or null — what `bongos upgrade` (upgrade.js main) asks before anything else.
|
|
68
|
+
async function operatorNotice({ instanceDir }, deps = {}) {
|
|
69
|
+
const door = await operatorRunsThisProject({ instanceDir }, deps);
|
|
70
|
+
return door ? operatorRunMessage(door) : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { readUpdateDoor, operatorRunsThisProject, operatorRunMessage, operatorNotice };
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -1433,7 +1433,7 @@ function parseArgs(argv) {
|
|
|
1433
1433
|
};
|
|
1434
1434
|
}
|
|
1435
1435
|
|
|
1436
|
-
async function main(argv = process.argv.slice(2)) {
|
|
1436
|
+
async function main(argv = process.argv.slice(2), deps = {}) {
|
|
1437
1437
|
if (hasFlag('--help', argv) || argv[0] === 'help') {
|
|
1438
1438
|
process.stdout.write([
|
|
1439
1439
|
'bongos upgrade — pull the Cloud Bongos core to a new version and apply it.',
|
|
@@ -1471,9 +1471,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
1471
1471
|
}
|
|
1472
1472
|
// Backward-compatible: bare `bongos upgrade` (no --to) is the documented module-compatibility
|
|
1473
1473
|
// PRE-CHECK — unchanged behavior, delegated to the module CLI. A --to switches to the real channel.
|
|
1474
|
+
// task 1004596: from a builder's own checkout of a project its OPERATOR runs, say who upgrades it and how to ask — never a server-only refusal (upgrade-operator.js has the condition).
|
|
1475
|
+
const operatorMsg = await (deps.operatorNotice || require('./upgrade-operator').operatorNotice)({ instanceDir: path.resolve(arg('--instance', argv) || process.cwd()) });
|
|
1474
1476
|
if (!arg('--to', argv)) {
|
|
1475
|
-
|
|
1477
|
+
const code = await (deps.cmdUpgrade || require('./module').cmdUpgrade)(argv);
|
|
1478
|
+
if (operatorMsg) process.stdout.write(`\n${operatorMsg}`);
|
|
1479
|
+
return code;
|
|
1476
1480
|
}
|
|
1481
|
+
if (operatorMsg) { process.stderr.write(`\n✖ ${operatorMsg}`); return 1; }
|
|
1477
1482
|
const res = await runUpgrade(parseArgs(argv));
|
|
1478
1483
|
if (!res.ok) { process.stderr.write(`\n✖ ${res.error || 'upgrade failed'}\n${require('./upgrade-outcome').outcomeLine(res)}`); return 1; } // the outcome line: how far it got, for the deploy runner (task 1004446)
|
|
1479
1484
|
return 0;
|
|
@@ -180,6 +180,9 @@ const CANNED_WRITES = [
|
|
|
180
180
|
// a vote cast from the street's matter card (task 1004528), so the card's
|
|
181
181
|
// answer (the toast, the building lighting) renders here too.
|
|
182
182
|
['POST', /^government\/board\/items\/\d+\/votes$/, 'government__board__items__votes.post'],
|
|
183
|
+
// Settings → Software update's "Ask for this upgrade" (task 1004595), so the
|
|
184
|
+
// pending line the panel draws after the press renders here too.
|
|
185
|
+
['POST', /^software-update\/request$/, 'software-update__request.post'],
|
|
183
186
|
];
|
|
184
187
|
|
|
185
188
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
|
@@ -110,6 +110,9 @@ const EXEMPT = [
|
|
|
110
110
|
/^\/downloads\//,
|
|
111
111
|
{ re: new RegExp(`^${API}/access-requests$`), methods: ['POST'] },
|
|
112
112
|
{ re: new RegExp(`^${API}/access-requests/status$`), methods: ['GET'] },
|
|
113
|
+
// The hub redeeming a `bongos feedback` receipt (task 1004591): it has no
|
|
114
|
+
// session here, and a members-only project must still vouch for its builders.
|
|
115
|
+
{ re: new RegExp(`^${API}/feedback/receipt/[a-z0-9_-]+$`), methods: ['GET'] },
|
|
113
116
|
];
|
|
114
117
|
|
|
115
118
|
// The policy, read through the memoized pack. A pack that cannot be read at all
|
|
@@ -33,6 +33,7 @@ const { readCoreUpdate, readCoreUpgrades } = require('../core-update');
|
|
|
33
33
|
const { readSoftwareUpdate } = require('../software-update');
|
|
34
34
|
const seams = require('../../module-seams');
|
|
35
35
|
const { loadIdpConfig } = require('../auth-config');
|
|
36
|
+
const upgradeRequest = require('../upgrade-request');
|
|
36
37
|
|
|
37
38
|
// WHERE A HOSTED PROJECT'S OWNER TAKES THE UPGRADE (task 1004174, ADR 0339): the hub's
|
|
38
39
|
// own deploy door, scoped to this project — `<hub>/deploy?project=<slug>`. The act stays
|
|
@@ -73,12 +74,22 @@ function hubManageLinks(config) {
|
|
|
73
74
|
// local — this hall runs the door: its own /deploy.
|
|
74
75
|
// hub — a hosted project: the hub's door for this project (ADR 0339 D3).
|
|
75
76
|
// cli — neither: a self-hosted instance, upgraded on its server with `bongos upgrade`.
|
|
76
|
-
|
|
77
|
+
// A hosted project that does not federate its sign-in (a cloud-host, or one keeping its own
|
|
78
|
+
// GitHub app) has no idp config to derive the hub from, so provisioning writes the door's
|
|
79
|
+
// address as <PREFIX>_HUB_UPGRADE_URL (task 1004608). Without it, those owners were told to
|
|
80
|
+
// run `bongos upgrade` on a server the platform runs. Only the hub's /deploy path is taken.
|
|
81
|
+
const HUB_UPGRADE_URL_RE = /^https?:\/\/[^\s"'<>/]+\/deploy\?project=[A-Za-z0-9._%-]+$/;
|
|
82
|
+
function updateDoor({ provisioning, idp, hubUpgradeUrl }) {
|
|
77
83
|
if (provisioning) return { kind: 'local', url: '/deploy' };
|
|
78
|
-
const url = hubDeployUrl(idp)
|
|
84
|
+
const url = hubDeployUrl(idp)
|
|
85
|
+
|| (typeof hubUpgradeUrl === 'string' && HUB_UPGRADE_URL_RE.test(hubUpgradeUrl.trim()) ? hubUpgradeUrl.trim() : null);
|
|
79
86
|
return url ? { kind: 'hub', url } : { kind: 'cli', url: null };
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
function safeHubUpgradeUrl() {
|
|
90
|
+
try { return require('../../instance-config').resolveEnv('HUB_UPGRADE_URL') || null; } catch { return null; }
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
function provisioningEnabled() {
|
|
83
94
|
try { return require('../../modules').isModuleEnabled('provisioning'); } catch { return false; }
|
|
84
95
|
}
|
|
@@ -164,16 +175,57 @@ async function resolveChannel(selfChannel) {
|
|
|
164
175
|
return { channel, source: channel ? 'deploy' : 'default' };
|
|
165
176
|
}
|
|
166
177
|
|
|
178
|
+
// "Ask for this upgrade" (task 1004595): what src/bongos/upgrade-request.js needs from
|
|
179
|
+
// the outside world, resolved per request so a test can stand every piece in. The receipt
|
|
180
|
+
// goes into the SAME store the public redeem route reads (routes/feedback.js).
|
|
181
|
+
function defaultAskDeps() {
|
|
182
|
+
const ps = require('../project-settings');
|
|
183
|
+
const fb = require('./feedback');
|
|
184
|
+
return {
|
|
185
|
+
hub: fb.feedbackHub(),
|
|
186
|
+
host: configuredHost(),
|
|
187
|
+
getSetting: ps.getSetting,
|
|
188
|
+
setSetting: ps.setSetting,
|
|
189
|
+
issueReceipt: (r) => fb.defaultReceiptStore().issue(r),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The host this project answers on, from its OWN branding pack (domains is server-only):
|
|
194
|
+
// the origin the hub calls back to redeem the receipt. Never read from the request: a
|
|
195
|
+
// Host or X-Forwarded-Host header is the caller's to write, and would let them point the
|
|
196
|
+
// hub's call-back at any address they like. '' when the pack names none.
|
|
197
|
+
function configuredHost() {
|
|
198
|
+
try {
|
|
199
|
+
const d = require('../../branding').branding().domains || {};
|
|
200
|
+
return new URL(d.publicOrigin || d.buildersOrigin).host;
|
|
201
|
+
} catch { return ''; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// The page's ask state. Fail-soft: a stored row that cannot be read is "not asked yet",
|
|
205
|
+
// never a 500 on the whole Software update panel.
|
|
206
|
+
async function readAsk({ update, door, deps }) {
|
|
207
|
+
const offered = update && update.reading === 'ok' ? update.offered : null;
|
|
208
|
+
const base = { offered, host: upgradeRequest.projectHost(deps.host), doorKind: door.kind, hub: deps.hub };
|
|
209
|
+
let stored = null;
|
|
210
|
+
if (offered && upgradeRequest.isOperatorRun(base)) {
|
|
211
|
+
try { stored = upgradeRequest.parseStored(await deps.getSetting(upgradeRequest.SETTING_KEY)); } catch { stored = null; }
|
|
212
|
+
}
|
|
213
|
+
return upgradeRequest.askState({ ...base, stored });
|
|
214
|
+
}
|
|
215
|
+
|
|
167
216
|
function buildCoreUpdateRouter({
|
|
168
217
|
coreUpdate = readCoreUpdate,
|
|
169
218
|
selfChannel = storedSelfChannel,
|
|
170
219
|
upgrades = readCoreUpgrades,
|
|
171
220
|
softwareUpdate = readSoftwareUpdate,
|
|
172
221
|
canUpdate = holdsCorePin,
|
|
173
|
-
doorInputs = () => ({ provisioning: provisioningEnabled(), idp: safeIdpConfig() }),
|
|
222
|
+
doorInputs = () => ({ provisioning: provisioningEnabled(), idp: safeIdpConfig(), hubUpgradeUrl: safeHubUpgradeUrl() }),
|
|
223
|
+
askDeps = defaultAskDeps,
|
|
174
224
|
} = {}) {
|
|
175
225
|
contributeCoreUpdateToDocket();
|
|
176
226
|
const router = express.Router();
|
|
227
|
+
// One send at a time: a double press waits for the first and is answered from its row.
|
|
228
|
+
let asking = null;
|
|
177
229
|
|
|
178
230
|
router.get(
|
|
179
231
|
'/core-update',
|
|
@@ -229,10 +281,51 @@ function buildCoreUpdateRouter({
|
|
|
229
281
|
resolveChannel(selfChannel).then(async ({ channel, source }) => ({ ...(await softwareUpdate({ running, channel })), channel_source: source })),
|
|
230
282
|
canUpdate(req),
|
|
231
283
|
]);
|
|
232
|
-
|
|
284
|
+
const door = updateDoor(doorInputs());
|
|
285
|
+
const ask = await readAsk({ update, door, deps: askDeps() });
|
|
286
|
+
res.json({ ok: true, update, door, can_update: can === true, ask });
|
|
233
287
|
}, { errorCode: 'software_update_failed', message: false }),
|
|
234
288
|
);
|
|
235
289
|
|
|
290
|
+
// POST /software-update/request — "Ask for this upgrade" (task 1004595). On a project
|
|
291
|
+
// the hub's operator runs, sends one request for the version this project is offered
|
|
292
|
+
// to the operator, through the direct feedback door, and remembers it so the page can
|
|
293
|
+
// say it is pending. The version is the server's own reading, never the caller's.
|
|
294
|
+
// Idempotent per version; a different version waits out a 24h cooldown (429).
|
|
295
|
+
// rank: core.pin.move — the same atom as the Update button the panel draws.
|
|
296
|
+
router.post(
|
|
297
|
+
'/software-update/request',
|
|
298
|
+
auth.requireBuilder,
|
|
299
|
+
auth.requirePermission('core.pin.move'),
|
|
300
|
+
asyncHandler('POST /software-update/request', async (req, res) => {
|
|
301
|
+
while (asking) { try { await asking; } catch { /* the next one tries for itself */ } }
|
|
302
|
+
const run = (async () => {
|
|
303
|
+
let running = null;
|
|
304
|
+
try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
|
|
305
|
+
const { channel } = await resolveChannel(selfChannel);
|
|
306
|
+
const update = await softwareUpdate({ running, channel });
|
|
307
|
+
const door = updateDoor(doorInputs());
|
|
308
|
+
const deps = askDeps();
|
|
309
|
+
return upgradeRequest.requestUpgrade({
|
|
310
|
+
host: deps.host,
|
|
311
|
+
running,
|
|
312
|
+
wanted: update && update.reading === 'ok' ? update.offered : null,
|
|
313
|
+
builder: req.builder,
|
|
314
|
+
doorKind: door.kind,
|
|
315
|
+
}, deps);
|
|
316
|
+
})();
|
|
317
|
+
asking = run;
|
|
318
|
+
let out;
|
|
319
|
+
try { out = await run; } finally { if (asking === run) asking = null; }
|
|
320
|
+
if (out.status >= 400) {
|
|
321
|
+
const { error, message, ...details } = out.body;
|
|
322
|
+
if (details.retry_after_seconds) res.set('Retry-After', String(details.retry_after_seconds));
|
|
323
|
+
return res.fail(error, { status: out.status, message, details });
|
|
324
|
+
}
|
|
325
|
+
res.status(out.status).json(out.body);
|
|
326
|
+
}, { errorCode: 'upgrade_request_failed', message: false }),
|
|
327
|
+
);
|
|
328
|
+
|
|
236
329
|
// GET /project/manage-link — the hub's page for THIS project, where its owner takes it
|
|
237
330
|
// offline or disconnects it (task 1004570). On a hosted project the owner is its Archon,
|
|
238
331
|
// so the read is Archon-only: a builder below that is refused and the hall draws no
|
|
@@ -254,4 +347,5 @@ function buildCoreUpdateRouter({
|
|
|
254
347
|
module.exports = buildCoreUpdateRouter;
|
|
255
348
|
module.exports.hubManageLinks = hubManageLinks;
|
|
256
349
|
module.exports.hubDeployUrl = hubDeployUrl;
|
|
350
|
+
module.exports.updateDoor = updateDoor;
|
|
257
351
|
module.exports.coreUpdateDocketMatters = coreUpdateDocketMatters;
|