@bongos/core 1.20.37 → 1.20.39
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/backlog-review/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +66 -0
- package/.claude/skills/collab-review/SKILL.md +1 -1
- package/.claude/skills/priority-session/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/status/SKILL.md +1 -1
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +12 -1
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
- package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +41 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +188 -2
- package/docs/api-reference.md +12 -4
- package/docs/copy-inventory.md +10 -10
- package/docs/copy-registry.json +18 -18
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +253 -250
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/modules/autonomy/db.js +39 -8
- package/modules/autonomy/fence.js +26 -4
- package/modules/autonomy/migrations/autonomy_003_priority_goal.sql +32 -0
- package/modules/autonomy/routes/autonomy.js +28 -1
- package/modules/hall-ui/public/gate.js +33 -4
- package/modules/ideas/routing.js +8 -1
- package/modules/npm-release/preview/childlog.js +95 -0
- package/modules/npm-release/preview/commands.js +46 -5
- package/modules/npm-release/preview/env.js +18 -7
- package/modules/npm-release/preview/runtime.js +43 -9
- package/modules/npm-release/preview/supervisor.js +170 -31
- package/modules/npm-release/public/work.css +3 -0
- package/modules/npm-release/public/work.js +6 -1
- package/modules/npm-release/routes/preview.js +1 -0
- package/modules/platform-identity/instance-feedback.js +142 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/sso.js +62 -0
- package/modules/provisioning/render-standup.js +13 -1
- package/modules/public-landing/public/projects.html +22 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +30 -0
- package/scripts/gds/autobongos-run.js +14 -4
- package/scripts/gds/feedback-send.js +131 -0
- package/scripts/gds/provision-render.js +13 -1
- package/scripts/gds/render-payload.js +6 -5
- package/scripts/gds/update-channel.js +82 -0
- package/src/bongos/routes/feedback.js +110 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/autobongos_fence.mjs +62 -0
- package/tests/autobongos_loop.mjs +76 -0
- package/tests/autonomy_fence_priority.mjs +211 -0
- package/tests/bongos_cli.mjs +7 -1
- package/tests/filing_abuse_matrix.mjs +7 -2
- package/tests/idea_routing.mjs +1 -1
- package/tests/instance_feedback.mjs +327 -0
- package/tests/npm_release_preview_commands.mjs +1 -1
- package/tests/npm_release_preview_diagnosis.mjs +376 -0
- package/tests/npm_release_preview_supervisor.mjs +4 -4
- package/tests/projects_hub_app_step.mjs +49 -0
- package/tests/projects_hub_render_connect.mjs +2 -1
- package/tests/provision_render.mjs +23 -2
- package/tests/update_channel_db.mjs +124 -0
|
@@ -31,6 +31,18 @@ const MODES = ['adopt', 'greenfield'];
|
|
|
31
31
|
// How the owner's app is built (task 1004350): a native runtime with their commands, or
|
|
32
32
|
// `docker`, which builds the repository's own Dockerfile.
|
|
33
33
|
const RUNTIMES = ['node', 'python', 'ruby', 'go', 'rust', 'elixir', 'docker'];
|
|
34
|
+
// Render refuses a native web service with no build command (measured live 2026-09-30,
|
|
35
|
+
// task 1004464), so every native runtime has one: Render's own default for it. The runner
|
|
36
|
+
// sends it when the owner gave none, and the wizard prefills it (projects.html keeps a
|
|
37
|
+
// copy, pinned equal by tests/projects_hub_app_step.mjs).
|
|
38
|
+
const BUILD_DEFAULTS = Object.freeze({
|
|
39
|
+
node: 'npm install',
|
|
40
|
+
python: 'pip install -r requirements.txt',
|
|
41
|
+
ruby: 'bundle install',
|
|
42
|
+
go: 'go build -tags netgo -ldflags \'-s -w\' -o app',
|
|
43
|
+
rust: 'cargo build --release',
|
|
44
|
+
elixir: 'mix deps.get --only prod && MIX_ENV=prod mix compile',
|
|
45
|
+
});
|
|
34
46
|
|
|
35
47
|
// Render API keys start `rnd_`; workspaces are `tea-…` (a team) or `usr-…` (a personal
|
|
36
48
|
// one); services `srv-…`. Shape checks only — whether the key works, and whether it can
|
|
@@ -134,4 +146,4 @@ async function attachToProvision(db, instanceId, { keyEnc, appHost }) {
|
|
|
134
146
|
return rows[0] || null;
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
module.exports = { attachToProvision, KEY_RE, OWNER_RE, WEB_PLANS, DB_PLANS, REGIONS, MODES, RUNTIMES, badRequest, appHostFrom, enqueueRenderStandup };
|
|
149
|
+
module.exports = { attachToProvision, KEY_RE, OWNER_RE, WEB_PLANS, DB_PLANS, REGIONS, MODES, RUNTIMES, BUILD_DEFAULTS, badRequest, appHostFrom, enqueueRenderStandup };
|
|
@@ -1433,7 +1433,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1433
1433
|
<option value="docker">Docker — use my repository’s Dockerfile</option>
|
|
1434
1434
|
</select>
|
|
1435
1435
|
</label>
|
|
1436
|
-
<label class="wizField" id="renderBuildWrap"><span>Build command <small>—
|
|
1436
|
+
<label class="wizField" id="renderBuildWrap"><span>Build command <small>— Render needs one; the usual one is filled in</small></span>
|
|
1437
1437
|
<input type="text" id="f-renderBuild" autocomplete="off" spellcheck="false" maxlength="500" placeholder="for example: npm ci">
|
|
1438
1438
|
</label>
|
|
1439
1439
|
<label class="wizField" id="renderStartWrap"><span>Start command</span>
|
|
@@ -1808,6 +1808,18 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1808
1808
|
function planPrice(p) { return p[2] ? 'about $' + p[2] + '/month' : RENDER_FREE_NOTE; }
|
|
1809
1809
|
var RENDER_DB_PLANS = [['basic_256mb', 'Basic 256 MB', 6], ['basic_1gb', 'Basic 1 GB', 19], ['basic_4gb', 'Basic 4 GB', 75]];
|
|
1810
1810
|
var RENDER_RUNTIMES = { node: 'Node.js', python: 'Python', ruby: 'Ruby', go: 'Go', rust: 'Rust', elixir: 'Elixir', docker: 'Docker' };
|
|
1811
|
+
/* Render refuses a native app with no build command (task 1004464), so the box starts
|
|
1812
|
+
with Render's own default for the runtime — a real value the owner can change, and
|
|
1813
|
+
one that follows the runtime until they do. A copy of render-standup.js's
|
|
1814
|
+
BUILD_DEFAULTS (this page cannot require it), pinned equal by a test. */
|
|
1815
|
+
var RENDER_BUILD_DEFAULTS = {
|
|
1816
|
+
node: 'npm install',
|
|
1817
|
+
python: 'pip install -r requirements.txt',
|
|
1818
|
+
ruby: 'bundle install',
|
|
1819
|
+
go: 'go build -tags netgo -ldflags \'-s -w\' -o app',
|
|
1820
|
+
rust: 'cargo build --release',
|
|
1821
|
+
elixir: 'mix deps.get --only prod && MIX_ENV=prod mix compile'
|
|
1822
|
+
};
|
|
1811
1823
|
var RENDER_KEY_RE = /^rnd_[A-Za-z0-9]{16,128}$/;
|
|
1812
1824
|
function planLabel(list, v) {
|
|
1813
1825
|
var p = list.filter(function (x) { return x[0] === v; })[0] || list[0];
|
|
@@ -1894,6 +1906,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1894
1906
|
q('f-renderRuntime').value = st.renderRuntime || 'node';
|
|
1895
1907
|
var docker = q('f-renderRuntime').value === 'docker';
|
|
1896
1908
|
q('renderBuildWrap').hidden = docker; q('renderStartWrap').hidden = docker;
|
|
1909
|
+
// the runtime's usual build command until the owner types their own (task 1004464)
|
|
1910
|
+
if (!st.renderBuildTouched) st.renderBuild = RENDER_BUILD_DEFAULTS[q('f-renderRuntime').value] || '';
|
|
1897
1911
|
q('f-renderBuild').value = st.renderBuild || '';
|
|
1898
1912
|
q('f-renderStart').value = st.renderStart || '';
|
|
1899
1913
|
fillPlans(q('f-renderPlan'), RENDER_WEB_PLANS, st.renderPlan || 'starter');
|
|
@@ -1975,13 +1989,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1975
1989
|
hooks.save(); syncNew();
|
|
1976
1990
|
});
|
|
1977
1991
|
q('renderNew').addEventListener('input', function (ev) {
|
|
1978
|
-
|
|
1979
|
-
if (ev.target.id === q('f-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
}
|
|
1992
|
+
var typed = null;
|
|
1993
|
+
if (ev.target.id === q('f-renderBuild').id) { st.renderBuild = ev.target.value; st.renderBuildTouched = true; typed = st.renderBuild; }
|
|
1994
|
+
if (ev.target.id === q('f-renderStart').id) { st.renderStart = ev.target.value; typed = st.renderStart; }
|
|
1995
|
+
// a "say how your app starts / is built" ask is answered the moment that box has a
|
|
1996
|
+
// command — it used to stay on screen until the next press of Next
|
|
1997
|
+
if (typed != null && String(typed).trim() && /\berr\b/.test(q('h-render').className)) hint('h-render', '');
|
|
1985
1998
|
hooks.save();
|
|
1986
1999
|
});
|
|
1987
2000
|
function validate() {
|
|
@@ -1993,6 +2006,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1993
2006
|
if (st.renderMode === 'adopt') {
|
|
1994
2007
|
if (!services.some(function (s) { return s.id === st.renderServiceId && repoOf(s.repo) === repo().toLowerCase(); })) return bad('Choose the app to link, or create a new one.');
|
|
1995
2008
|
} else if (st.renderMode === 'greenfield') {
|
|
2009
|
+
if ((st.renderRuntime || 'node') !== 'docker' && !String(st.renderBuild || '').trim()) return bad('Say how your app is built — Render needs a build command, for example “npm install”.');
|
|
1996
2010
|
if ((st.renderRuntime || 'node') !== 'docker' && !String(st.renderStart || '').trim()) return bad('Say how your app starts — the command Render should run, for example “npm start”.');
|
|
1997
2011
|
} else return bad('Choose whether to create a new app or link one you already run.');
|
|
1998
2012
|
hint('h-render', '');
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.39",
|
|
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.39",
|
|
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.39",
|
|
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
|
@@ -8313,5 +8313,35 @@
|
|
|
8313
8313
|
"id": "1004406",
|
|
8314
8314
|
"text": "The unattended runner stops restarting every time anyone merges: it now updates its code in place and only restarts when its own code has changed."
|
|
8315
8315
|
}
|
|
8316
|
+
],
|
|
8317
|
+
"1.20.38": [
|
|
8318
|
+
{
|
|
8319
|
+
"id": "1004468",
|
|
8320
|
+
"text": "The nightly auto-update now follows the update rule you set on /deploy for each project, instead of a file on the server."
|
|
8321
|
+
},
|
|
8322
|
+
{
|
|
8323
|
+
"id": "1004453",
|
|
8324
|
+
"text": "When you start Autobongos from the Gate page you can now pick a priority goal: the runner works that goal first and moves to the other allowed goals only when it runs out of work there."
|
|
8325
|
+
},
|
|
8326
|
+
{
|
|
8327
|
+
"id": "1004472",
|
|
8328
|
+
"text": "Fixes the broken automatic check on main that was stopping everyone's finished work from landing."
|
|
8329
|
+
},
|
|
8330
|
+
{
|
|
8331
|
+
"text": "When a builder complains that Bongos isn't working, their assistant now reports it to the Bongos team by itself, so every frustration becomes something on the fix list."
|
|
8332
|
+
},
|
|
8333
|
+
{
|
|
8334
|
+
"text": "Builders on any Cloud Bongos project can now send feedback about Bongos itself with one command, and it arrives with the Bongos team as a task waiting for approval."
|
|
8335
|
+
}
|
|
8336
|
+
],
|
|
8337
|
+
"1.20.39": [
|
|
8338
|
+
{
|
|
8339
|
+
"id": "1004465",
|
|
8340
|
+
"text": "A version preview that cannot read its data now fails with the child's own output shown, and stopping it really removes its copy."
|
|
8341
|
+
},
|
|
8342
|
+
{
|
|
8343
|
+
"id": "1004464",
|
|
8344
|
+
"text": "Setting up an app on Render from the project wizard no longer fails when the owner leaves the build command empty. The box now comes filled in with Render's usual build command for the chosen language, which the owner can chan"
|
|
8345
|
+
}
|
|
8316
8346
|
]
|
|
8317
8347
|
}
|
|
@@ -106,6 +106,15 @@ function record(entry) {
|
|
|
106
106
|
return row;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// pickedFrom — the row fields that say where a pick came from (task 1004453).
|
|
110
|
+
// `priority` is true only when the fence named a priority goal AND this pick came
|
|
111
|
+
// from it, so "the priority goal ran dry and the runner moved on" reads as
|
|
112
|
+
// priority:false beside the goal it moved on to.
|
|
113
|
+
function pickedFrom(goalId, gate) {
|
|
114
|
+
const p = gate && gate.priorityGoal != null ? gate.priorityGoal : null;
|
|
115
|
+
return { priority: p !== null && Number(goalId) === p };
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
// pickTask — the allowlisted goals, in order, through sequence.js's halt matrix.
|
|
110
119
|
// The matrix is reused rather than re-derived: it already refuses thin specs,
|
|
111
120
|
// protected paths, judgement kinds and unmet dependencies, from facts on the row.
|
|
@@ -479,7 +488,7 @@ async function iteration(opts, deps = {}) {
|
|
|
479
488
|
// looks like, and it must still reach the failure ladder (task 1004179).
|
|
480
489
|
if (lastRefusal) return lastRefusal;
|
|
481
490
|
return log({
|
|
482
|
-
event: 'nothing_claimable', goals: gate.goals,
|
|
491
|
+
event: 'nothing_claimable', goals: gate.goals, priority_goal: gate.priorityGoal ?? null,
|
|
483
492
|
// Carried so the cadence can tell an EMPTY queue from one where every task
|
|
484
493
|
// halts — the reasons are the matrix's own codes, safe to print.
|
|
485
494
|
skipped: (got.skipped || []).map((sk) => ({ id: sk.id, goal_id: sk.goal_id, reasons: (sk.reasons || []).map((r) => r.code) })),
|
|
@@ -501,10 +510,10 @@ async function iteration(opts, deps = {}) {
|
|
|
501
510
|
task: candidate, goals: gate.goals, protectedHits: matchProtected(candidate.touches || []),
|
|
502
511
|
});
|
|
503
512
|
if (!taskGate.go) {
|
|
504
|
-
return log({ event: 'fenced', code: taskGate.code, reason: taskGate.reason, task_id: candidate.id, goal_id: got.goalId, globs: taskGate.globs || [] });
|
|
513
|
+
return log({ event: 'fenced', code: taskGate.code, reason: taskGate.reason, task_id: candidate.id, goal_id: got.goalId, ...pickedFrom(got.goalId, gate), globs: taskGate.globs || [] });
|
|
505
514
|
}
|
|
506
515
|
|
|
507
|
-
if (opts.dryRun) return log({ event: 'dry_run', task_id: candidate.id, title: candidate.title, goal_id: got.goalId });
|
|
516
|
+
if (opts.dryRun) return log({ event: 'dry_run', task_id: candidate.id, title: candidate.title, goal_id: got.goalId, ...pickedFrom(got.goalId, gate) });
|
|
508
517
|
|
|
509
518
|
// 3. The task's own working tree.
|
|
510
519
|
const name = deps.worktreeName ? deps.worktreeName(candidate.id) : worktreeName(candidate.id);
|
|
@@ -566,6 +575,7 @@ async function iteration(opts, deps = {}) {
|
|
|
566
575
|
|
|
567
576
|
const row = {
|
|
568
577
|
event: 'worked', task_id: task.id, title: task.title, goal_id: picked.goalId,
|
|
578
|
+
...pickedFrom(picked.goalId, gate),
|
|
569
579
|
worktree: wtName,
|
|
570
580
|
outcome: verdict.outcome, reason: verdict.reason,
|
|
571
581
|
session_id: res.sessionId || null, duration_s: Math.round((Date.now() - started) / 1000),
|
|
@@ -1043,7 +1053,7 @@ async function forever(opts, deps = {}) {
|
|
|
1043
1053
|
// task 1234" from "quiet because it is dead". A worker may hold the loop for
|
|
1044
1054
|
// ninety minutes, and without this every honest hour of work looks like a
|
|
1045
1055
|
// corpse.
|
|
1046
|
-
await beat({ mode: state.mode, last_event: row.event, working_task_id: Number(row.task_id) || undefined });
|
|
1056
|
+
await beat({ mode: state.mode, last_event: row.event, working_task_id: Number(row.task_id) || undefined, working_goal_id: Number(row.goal_id) || undefined });
|
|
1047
1057
|
const seen = cadence.classifyEvent(row);
|
|
1048
1058
|
// A hold that names its reset SECOND waits until that second (task 1004454).
|
|
1049
1059
|
// classifyEvent carries it as an absolute epoch; the cadence has no clock of
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Send feedback about Bongos itself to the Cloud Bongos maintainers (task 1004462).
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// bongos feedback "the claim command hung for two minutes"
|
|
6
|
+
// bongos feedback --idea "let me filter the task list by goal"
|
|
7
|
+
// bongos feedback --message-file notes.md
|
|
8
|
+
// [--json]
|
|
9
|
+
//
|
|
10
|
+
// Not the /feedback skill, which loads a locally recorded walkthrough into a
|
|
11
|
+
// session. This SENDS: it posts to the builder's OWN project (POST /feedback,
|
|
12
|
+
// src/bongos/routes/feedback.js), which relays it to the hub. A bug lands on the
|
|
13
|
+
// hub as a 'backlog' task in the maintenance goal awaiting a maintainer's nod; an
|
|
14
|
+
// idea lands in the hub's idea inbox. Only the typed text and version stamps
|
|
15
|
+
// travel — never code, a transcript or file contents.
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const os = require('node:os');
|
|
20
|
+
const { cliClient, requireSession, argText, hasFlag, errCode, errDetails, cliExit } = require('./cli-lib');
|
|
21
|
+
|
|
22
|
+
const USAGE = [
|
|
23
|
+
'usage:',
|
|
24
|
+
' bongos feedback "what went wrong" report a problem with Bongos',
|
|
25
|
+
' bongos feedback --idea "what you wish for" suggest an improvement',
|
|
26
|
+
' bongos feedback --message-file notes.md send a longer note from a file',
|
|
27
|
+
'',
|
|
28
|
+
'Goes to the Cloud Bongos maintainers. Only the text you type and the Bongos',
|
|
29
|
+
'versions you run are sent — no code, chat or files from your project.',
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
// The CLI release, as `bongos version` reports it: config/bongos-cli.json where
|
|
33
|
+
// the core checkout carries it (an instance does not — task 1824), else the
|
|
34
|
+
// package version. Read from disk, never required, so a vanilla core still runs.
|
|
35
|
+
function cliVersion() {
|
|
36
|
+
const fs = require('node:fs');
|
|
37
|
+
const path = require('node:path');
|
|
38
|
+
for (const rel of ['../../config/bongos-cli.json', '../../package.json']) {
|
|
39
|
+
try {
|
|
40
|
+
const f = path.join(__dirname, rel);
|
|
41
|
+
if (fs.existsSync(f)) { const v = JSON.parse(fs.readFileSync(f, 'utf8')).version; if (v) return v; }
|
|
42
|
+
} catch { /* try the next */ }
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The message: --message / --message-file, else every positional word joined.
|
|
48
|
+
function readMessage(args) {
|
|
49
|
+
if (args.includes('--message') || args.includes('--message-file')) return argText('--message', args);
|
|
50
|
+
return args.filter((a) => !a.startsWith('--')).join(' ');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Every report goes through the transcript-grade secret scrubber before it leaves
|
|
54
|
+
// this machine (task 1004467). An assistant may file unasked when a builder vents,
|
|
55
|
+
// so nobody reviews the text first — the scrubber is the check that does not
|
|
56
|
+
// depend on the assistant's judgment. FAIL CLOSED: no scrubber, nothing is sent.
|
|
57
|
+
// It also strips the builder's environment secrets by value, whatever their shape.
|
|
58
|
+
function scrubReport(message, env = process.env) {
|
|
59
|
+
let scrub;
|
|
60
|
+
try { scrub = require('../../modules/security/secret-scrub.js'); } catch { return null; }
|
|
61
|
+
const text = scrub.scrubSecrets(message, scrub.collectEnvSecrets(env));
|
|
62
|
+
const removed = text.split(scrub.REDACTED).length - message.split(scrub.REDACTED).length;
|
|
63
|
+
return { text, removed };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Plain-language lines for each refusal the route can give.
|
|
67
|
+
const REFUSALS = {
|
|
68
|
+
message_required: 'Nothing to send — add your feedback after the command, in quotes.',
|
|
69
|
+
validation_failed: 'That could not be sent as written — keep it under 4,000 characters, or split it into two.',
|
|
70
|
+
not_hub_connected: 'This project is not connected to Cloud Bongos, so feedback cannot be relayed from here.\n'
|
|
71
|
+
+ 'Report it to the Bongos maintainers on the Bongos core repository instead.',
|
|
72
|
+
membership_required: 'Cloud Bongos does not yet know you as a member of this project.\n'
|
|
73
|
+
+ 'Sign in to this project once through Cloud Bongos in a browser, then try again.',
|
|
74
|
+
rate_limited: 'Too much feedback from you or this project in the last hour. Try again later.',
|
|
75
|
+
feedback_unavailable: 'Cloud Bongos is not taking bug reports right now (no version is being built). Try again later.',
|
|
76
|
+
hub_unreachable: 'Could not reach Cloud Bongos. Check your connection and try again.',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
async function main(argv = process.argv.slice(2)) {
|
|
80
|
+
if (hasFlag('--help', argv) || hasFlag('-h', argv)) { console.log(USAGE); return 0; }
|
|
81
|
+
const json = hasFlag('--json', argv);
|
|
82
|
+
const kind = hasFlag('--idea', argv) ? 'idea' : 'bug';
|
|
83
|
+
const message = String(readMessage(argv) || '').trim();
|
|
84
|
+
if (!message) { console.error(USAGE); return 2; }
|
|
85
|
+
|
|
86
|
+
const scrubbed = scrubReport(message);
|
|
87
|
+
if (!scrubbed) {
|
|
88
|
+
console.error('Feedback was not sent: the secret filter that checks every report is missing from this install.');
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await requireSession();
|
|
93
|
+
const api = await cliClient();
|
|
94
|
+
const res = await api.request('POST', '/feedback', {
|
|
95
|
+
body: { message: scrubbed.text, kind, context: { cli_version: cliVersion(), os: `${os.platform()} ${os.release()}` } },
|
|
96
|
+
});
|
|
97
|
+
if (scrubbed.removed > 0 && !json) {
|
|
98
|
+
console.log(`Removed ${scrubbed.removed} secret-looking value(s) from the report before sending.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (json) { console.log(JSON.stringify(res.data, null, 2)); return res.ok ? 0 : 1; }
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
const code = errCode(res.data);
|
|
104
|
+
const details = errDetails(res.data) || {};
|
|
105
|
+
let line = REFUSALS[code] || `Feedback was not sent (${code || `HTTP ${res.status}`}).`;
|
|
106
|
+
if (code === 'rate_limited' && details.retry_after_seconds) {
|
|
107
|
+
line += ` (in about ${Math.ceil(details.retry_after_seconds / 60)} min)`;
|
|
108
|
+
}
|
|
109
|
+
console.error(line);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
const d = res.data || {};
|
|
113
|
+
if (d.landed === 'task') {
|
|
114
|
+
console.log(`Thanks — sent to the Cloud Bongos maintainers as task ${d.task_id}.`);
|
|
115
|
+
console.log('It waits for a maintainer to approve it before anyone works on it.');
|
|
116
|
+
if (d.hub_origin) console.log(`${String(d.hub_origin).replace(/\/+$/, '')}/builders#/task/${d.task_id}`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(`Thanks — your idea is in the Cloud Bongos idea inbox (idea ${d.idea_id}) for the maintainers to review.`);
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (require.main === module) {
|
|
124
|
+
// cliExit, never process.exit, after a request (task 1002734 — the Windows abort).
|
|
125
|
+
main().then((code) => cliExit(code), (err) => {
|
|
126
|
+
console.error(`Feedback was not sent: ${err && err.message ? err.message : err}`);
|
|
127
|
+
cliExit(1);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { main, readMessage, REFUSALS, scrubReport };
|
|
@@ -117,6 +117,18 @@ async function greenfield(api, inst, host, deps) {
|
|
|
117
117
|
return { ok: true };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// The owner reads this reason on the setup screen (app-status.js oneSentence). A refusal
|
|
121
|
+
// from Render's API used to arrive as its raw line — "Render POST /services failed: …" —
|
|
122
|
+
// so an answer from Render is re-said as a sentence with Render's own reason kept whole
|
|
123
|
+
// (task 1004464). Anything else is already the leg's own words.
|
|
124
|
+
function plainReason(e) {
|
|
125
|
+
const msg = e && e.message ? e.message : String(e);
|
|
126
|
+
const said = e && e.status != null && e.body && typeof e.body.message === 'string' ? e.body.message.trim() : '';
|
|
127
|
+
if (said) return `Render would not set up the app: ${said}`;
|
|
128
|
+
const m = /^Render (?:GET|POST|PATCH|PUT|DELETE) \S+ failed: ([\s\S]+)$/.exec(msg);
|
|
129
|
+
return m ? `Render would not set up the app: ${m[1].trim()}` : msg;
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
async function renderStandupInstance(inst, deps, intent) {
|
|
121
133
|
const { db, log } = deps;
|
|
122
134
|
const secretBox = deps.secretBox || require('../../src/bongos/secret-box');
|
|
@@ -137,7 +149,7 @@ async function renderStandupInstance(inst, deps, intent) {
|
|
|
137
149
|
|
|
138
150
|
return host.mode === 'adopt' ? await adopt(api, inst, host, deps) : await greenfield(api, inst, host, deps);
|
|
139
151
|
} catch (e) {
|
|
140
|
-
return refuse(
|
|
152
|
+
return refuse(plainReason(e));
|
|
141
153
|
} finally {
|
|
142
154
|
if (intent && intent.id != null) await clearBorrowedKey(db, intent.id);
|
|
143
155
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
'use strict';
|
|
22
22
|
|
|
23
23
|
// The vocabulary lives with the web tier's validation, so the two can never disagree.
|
|
24
|
-
const { WEB_PLANS, DB_PLANS, REGIONS, RUNTIMES } = require('../../modules/provisioning/render-standup.js');
|
|
24
|
+
const { WEB_PLANS, DB_PLANS, REGIONS, RUNTIMES, BUILD_DEFAULTS } = require('../../modules/provisioning/render-standup.js');
|
|
25
25
|
const DEFAULTS = Object.freeze({ webPlan: 'starter', dbPlan: 'basic_256mb', region: 'virginia' });
|
|
26
26
|
|
|
27
27
|
// A Render resource name from the project's slug. Render allows letters, digits and
|
|
@@ -47,14 +47,15 @@ function plansFor(appHost) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// How the app is built and started, as the owner said. `docker` builds the repository's
|
|
50
|
-
// own Dockerfile and needs no commands; every other runtime needs a start command
|
|
51
|
-
// build command
|
|
50
|
+
// own Dockerfile and needs no commands; every other runtime needs a start command AND a
|
|
51
|
+
// build command — Render refuses a native service without one (task 1004464), so a row
|
|
52
|
+
// saved with none (including every row from before that fix) gets the runtime's default.
|
|
52
53
|
function buildFor(appHost) {
|
|
53
54
|
const h = appHost || {};
|
|
54
55
|
if (!RUNTIMES.includes(h.runtime)) throw new Error(`unknown Render runtime '${h.runtime || 'none'}'`);
|
|
55
56
|
if (h.runtime === 'docker') return { runtime: 'docker' };
|
|
56
57
|
if (!h.startCommand) throw new Error('the app needs a start command');
|
|
57
|
-
return { runtime: h.runtime, buildCommand: h.buildCommand ||
|
|
58
|
+
return { runtime: h.runtime, buildCommand: h.buildCommand || BUILD_DEFAULTS[h.runtime], startCommand: h.startCommand };
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// POST /postgres. `plan` is never 'free' — see the header.
|
|
@@ -68,7 +69,7 @@ function renderPostgresPayload(inst, { ownerId, dbPlan, region }) {
|
|
|
68
69
|
function renderServicePayload(inst, { ownerId, repoUrl, branch, webPlan, region, build, dbUrl }) {
|
|
69
70
|
const envSpecificDetails = build.runtime === 'docker'
|
|
70
71
|
? {}
|
|
71
|
-
: {
|
|
72
|
+
: { buildCommand: build.buildCommand, startCommand: build.startCommand };
|
|
72
73
|
return {
|
|
73
74
|
type: 'web_service',
|
|
74
75
|
name: resourceName(inst),
|
|
@@ -228,6 +228,82 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
|
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// ---- the per-project rule set on /deploy (task 1004468, ADR 0357) -----------
|
|
232
|
+
// The owner sets each project's update rule on /deploy, which writes
|
|
233
|
+
// provisioning_instances.update_channel (migration provisioning_029, task 1004445). That column
|
|
234
|
+
// is the rule; the roster's `channel` is only the fallback for an entry no project row matches,
|
|
235
|
+
// or for a sweep that cannot reach the database. Before this, the button followed the column and
|
|
236
|
+
// the sweep followed the roster, so a project set to `minor` on /deploy was still swept as `patch`.
|
|
237
|
+
//
|
|
238
|
+
// Matched by slug — the one stable key the table has (it records no directory). Read over psql,
|
|
239
|
+
// which is how upgrade.js recordLedger reaches a PGDATABASE (a bare name over the unix socket +
|
|
240
|
+
// peer auth, the self-host shape). recordLedger uses the pg driver for DATABASE_URL; this passes
|
|
241
|
+
// the URL to `psql -d` instead, because the sweep is synchronous and pg is not. With no psql the
|
|
242
|
+
// sweep falls back to the roster and says so.
|
|
243
|
+
//
|
|
244
|
+
// A failure's reason is a fixed class, never psql's own text: stderr can name a DB user or host,
|
|
245
|
+
// and the reason is printed into the sweep's log.
|
|
246
|
+
const CHANNEL_SOURCE_DEPLOY = 'deploy';
|
|
247
|
+
const CHANNEL_SOURCE_ROSTER = 'roster';
|
|
248
|
+
const DB_CHANNELS_SQL = 'SELECT slug, update_channel FROM provisioning_instances';
|
|
249
|
+
const DB_CHANNELS_TIMEOUT_MS = 15000;
|
|
250
|
+
|
|
251
|
+
// PURE. psql's stderr reduced to a class that is safe to log.
|
|
252
|
+
function psqlFailureClass(stderr) {
|
|
253
|
+
const s = String(stderr || '');
|
|
254
|
+
if (/relation .*provisioning_instances.* does not exist/i.test(s)) return 'no provisioning_instances table (provisioning module off?)';
|
|
255
|
+
if (/column .*update_channel.* does not exist/i.test(s)) return 'no update_channel column (migration provisioning_029 not applied)';
|
|
256
|
+
if (/authentication failed|role .* does not exist|permission denied/i.test(s)) return 'the sweep\'s database login was refused';
|
|
257
|
+
if (/could not connect|connection refused|no such file or directory|could not translate host/i.test(s)) return 'database unreachable';
|
|
258
|
+
if (/database .* does not exist/i.test(s)) return 'the named database does not exist';
|
|
259
|
+
return 'query failed';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The line a sweep prints for one entry's channel. PURE.
|
|
263
|
+
function channelLine(inst) {
|
|
264
|
+
return ` • ${inst.slug}: channel ${inst.channel}, ${inst.channelNote}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Read every project's update rule. Never throws: { ok:true, channels: Map<slug, channel> }, or
|
|
268
|
+
// { ok:false, reason } when there is no database to ask, psql is missing, or the table is absent
|
|
269
|
+
// (an instance with the provisioning module off). The caller falls back to the roster and says why.
|
|
270
|
+
function readDbChannels({ env = process.env, run = spawnSync } = {}) {
|
|
271
|
+
const target = env.DATABASE_URL || env.PGDATABASE;
|
|
272
|
+
if (!target) return { ok: false, reason: 'no DATABASE_URL or PGDATABASE set for the sweep' };
|
|
273
|
+
const r = run('psql', ['-d', target, '-At', '-F', '\t', '-v', 'ON_ERROR_STOP=1', '--no-psqlrc', '-c', DB_CHANNELS_SQL],
|
|
274
|
+
{ encoding: 'utf8', timeout: DB_CHANNELS_TIMEOUT_MS });
|
|
275
|
+
if (!r || r.error) {
|
|
276
|
+
const code = r && r.error && r.error.code;
|
|
277
|
+
return { ok: false, reason: code === 'ENOENT' ? 'psql not found' : code === 'ETIMEDOUT' ? 'psql timed out' : 'psql could not be started' };
|
|
278
|
+
}
|
|
279
|
+
if (r.status !== 0) return { ok: false, reason: `psql exit ${r.status}: ${psqlFailureClass(r.stderr)}` };
|
|
280
|
+
const channels = new Map();
|
|
281
|
+
for (const line of String(r.stdout || '').split('\n')) {
|
|
282
|
+
const [slug, ch] = line.split('\t');
|
|
283
|
+
if (slug && ch) channels.set(slug.trim(), ch.trim());
|
|
284
|
+
}
|
|
285
|
+
return { ok: true, channels };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// PURE. Give each roster entry the channel it is actually under, and say where it came from:
|
|
289
|
+
// `channelSource` is 'deploy' (the project row) or 'roster', `channelNote` the one-line reason a
|
|
290
|
+
// sweep log prints. The roster's own value is kept as `rosterChannel`, so a disagreement stays
|
|
291
|
+
// visible in the log instead of silently vanishing.
|
|
292
|
+
function applyDbChannels(instances, db) {
|
|
293
|
+
return instances.map((inst) => {
|
|
294
|
+
if (db && db.ok && db.channels.has(inst.slug)) {
|
|
295
|
+
const ch = normalizeChannel(db.channels.get(inst.slug));
|
|
296
|
+
const differs = ch !== inst.channel;
|
|
297
|
+
return {
|
|
298
|
+
...inst, channel: ch, channelSource: CHANNEL_SOURCE_DEPLOY, rosterChannel: inst.channel,
|
|
299
|
+
channelNote: `set on /deploy${differs ? ` (roster says ${inst.channel}, ignored)` : ''}`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const why = db && db.ok ? 'no project row for this slug' : (db && db.reason) || 'database not read';
|
|
303
|
+
return { ...inst, channelSource: CHANNEL_SOURCE_ROSTER, rosterChannel: inst.channel, channelNote: `from the roster — ${why}` };
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
231
307
|
// ---- reporting: make "nothing happened" audible -----------------------------
|
|
232
308
|
|
|
233
309
|
// Turn a loadSubscriptions() result into the line a routine prints EVERY run, plus a level a caller
|
|
@@ -322,6 +398,12 @@ module.exports = {
|
|
|
322
398
|
listAvailableVersions,
|
|
323
399
|
parseNpmViewVersions,
|
|
324
400
|
loadSubscriptions,
|
|
401
|
+
readDbChannels,
|
|
402
|
+
applyDbChannels,
|
|
403
|
+
channelLine,
|
|
404
|
+
psqlFailureClass,
|
|
405
|
+
CHANNEL_SOURCE_DEPLOY,
|
|
406
|
+
CHANNEL_SOURCE_ROSTER,
|
|
325
407
|
ROSTER_OK,
|
|
326
408
|
ROSTER_ABSENT,
|
|
327
409
|
ROSTER_MALFORMED,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/routes/feedback.js — POST /api/bongos/feedback (task 1004462).
|
|
4
|
+
//
|
|
5
|
+
// `bongos feedback "…"` lands here, on the builder's OWN project, with the session
|
|
6
|
+
// they already hold. This route stamps who sent it and which core this project
|
|
7
|
+
// runs, then relays it to the hub's POST /sso/feedback over the hub-federation
|
|
8
|
+
// client credential (ADR 0141) — the channel the activity rollup already uses. So
|
|
9
|
+
// a builder never signs in to cloudbongos.com to report something about Bongos.
|
|
10
|
+
//
|
|
11
|
+
// Only the text the builder typed and version stamps travel. No code, transcript,
|
|
12
|
+
// task or file content from this project is attached.
|
|
13
|
+
//
|
|
14
|
+
// In the CORE, not a module, for the reason core-update.js gives: the hub link is
|
|
15
|
+
// kernel (src/bongos/auth-config.js), and every hosted project must have this door
|
|
16
|
+
// whatever modules it runs. A project with no hub link (self-hosted, or the hub
|
|
17
|
+
// itself) answers 409 not_hub_connected — told, not silently dropped.
|
|
18
|
+
|
|
19
|
+
const express = require('express');
|
|
20
|
+
const { asyncHandler, validateOrRespond } = require('./_helpers');
|
|
21
|
+
const auth = require('../auth');
|
|
22
|
+
const { loadIdpConfig } = require('../auth-config');
|
|
23
|
+
const { userAgent } = require('../../branding');
|
|
24
|
+
|
|
25
|
+
const MESSAGE_MAX = 4000;
|
|
26
|
+
const HUB_TIMEOUT_MS = 15000;
|
|
27
|
+
|
|
28
|
+
function safeIdpConfig() {
|
|
29
|
+
try { return loadIdpConfig(); } catch { return null; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runningCore() {
|
|
33
|
+
try { return require('../../module-api').CORE_VERSION || null; } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Hub refusals the builder can act on are passed through by code; anything else
|
|
37
|
+
// is one generic failure, so the hub's internals never leak through this door.
|
|
38
|
+
const PASS_THROUGH = new Set(['membership_required', 'rate_limited', 'feedback_unavailable', 'validation_failed']);
|
|
39
|
+
|
|
40
|
+
async function relayToHub({ idp, builder, message, kind, context, fetchImpl = fetch }) {
|
|
41
|
+
const res = await fetchImpl(`${idp.origin.replace(/\/+$/, '')}/api/bongos/sso/feedback`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': userAgent() },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
client_id: idp.clientId,
|
|
46
|
+
client_secret: idp.clientSecret,
|
|
47
|
+
github_id: builder.github_id,
|
|
48
|
+
message,
|
|
49
|
+
kind,
|
|
50
|
+
context,
|
|
51
|
+
}),
|
|
52
|
+
signal: AbortSignal.timeout(HUB_TIMEOUT_MS),
|
|
53
|
+
});
|
|
54
|
+
let body = null;
|
|
55
|
+
try { body = await res.json(); } catch { /* non-JSON answer */ }
|
|
56
|
+
return { status: res.status, body };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildFeedbackRouter({ idpConfig = safeIdpConfig, fetchImpl = fetch } = {}) {
|
|
60
|
+
const router = express.Router();
|
|
61
|
+
|
|
62
|
+
// rank: any signed-in builder — reporting a problem with Bongos grants nothing
|
|
63
|
+
// here, and the hub files every report at 'backlog' behind a Metic+ nod.
|
|
64
|
+
router.post(
|
|
65
|
+
'/feedback',
|
|
66
|
+
auth.requireBuilder,
|
|
67
|
+
asyncHandler('POST /feedback', async (req, res) => {
|
|
68
|
+
if (validateOrRespond(req, res, {
|
|
69
|
+
message: { required: true, type: 'string', maxLength: MESSAGE_MAX },
|
|
70
|
+
kind: { type: 'string', enum: ['bug', 'idea'] },
|
|
71
|
+
context: {},
|
|
72
|
+
})) return;
|
|
73
|
+
const b = req.body || {};
|
|
74
|
+
const message = b.message.trim();
|
|
75
|
+
if (!message) return res.fail('message_required', 400);
|
|
76
|
+
const kind = b.kind || 'bug';
|
|
77
|
+
|
|
78
|
+
const idp = idpConfig();
|
|
79
|
+
if (!idp) return res.fail('not_hub_connected', 409);
|
|
80
|
+
|
|
81
|
+
const ctx = b.context && typeof b.context === 'object' ? b.context : {};
|
|
82
|
+
const context = {
|
|
83
|
+
core_version: runningCore(),
|
|
84
|
+
cli_version: typeof ctx.cli_version === 'string' ? ctx.cli_version.slice(0, 40) : null,
|
|
85
|
+
os: typeof ctx.os === 'string' ? ctx.os.slice(0, 40) : null,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let hub;
|
|
89
|
+
try {
|
|
90
|
+
hub = await relayToHub({ idp, builder: req.builder, message, kind, context, fetchImpl });
|
|
91
|
+
} catch {
|
|
92
|
+
return res.fail('hub_unreachable', 502);
|
|
93
|
+
}
|
|
94
|
+
if (hub.status === 201 && hub.body && hub.body.ok) {
|
|
95
|
+
const { ok: _ok, ...landing } = hub.body;
|
|
96
|
+
return res.status(201).json({ ok: true, hub_origin: idp.origin, ...landing });
|
|
97
|
+
}
|
|
98
|
+
const err = hub.body && hub.body.error && typeof hub.body.error === 'object' ? hub.body.error : null;
|
|
99
|
+
if (err && PASS_THROUGH.has(err.code)) {
|
|
100
|
+
const retry = err.details && err.details.retry_after_seconds;
|
|
101
|
+
return res.fail(err.code, hub.status, retry ? { retry_after_seconds: retry } : undefined);
|
|
102
|
+
}
|
|
103
|
+
return res.fail('hub_refused', 502, { hub_status: hub.status });
|
|
104
|
+
}, { errorCode: 'feedback_failed', message: false }),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return router;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = buildFeedbackRouter;
|
package/src/bongos/routes.js
CHANGED
|
@@ -20,6 +20,7 @@ const express = require('express');
|
|
|
20
20
|
const buildHealthzRouter = require('./routes/healthz');
|
|
21
21
|
const buildInstanceRouter = require('./routes/instance');
|
|
22
22
|
const buildCoreUpdateRouter = require('./routes/core-update');
|
|
23
|
+
const buildFeedbackRouter = require('./routes/feedback');
|
|
23
24
|
const buildAuthRouter = require('./routes/auth');
|
|
24
25
|
const buildGithubRouter = require('./routes/github');
|
|
25
26
|
const buildMeRouter = require('./routes/me');
|
|
@@ -144,6 +145,7 @@ function buildGdsRouter() {
|
|
|
144
145
|
router.use(buildHealthzRouter());
|
|
145
146
|
router.use(buildInstanceRouter());
|
|
146
147
|
router.use(buildCoreUpdateRouter());
|
|
148
|
+
router.use(buildFeedbackRouter());
|
|
147
149
|
router.use(buildAuthRouter());
|
|
148
150
|
router.use(buildGithubRouter());
|
|
149
151
|
router.use(buildMeRouter());
|
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.39'; // 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');
|