@bongos/core 1.21.92 → 1.21.94
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 +86 -56
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +1 -0
- package/docs/api/openapi.json +38 -7
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +354 -354
- package/docs/copy-registry.json +420 -420
- package/docs/design/project-startup-direction.md +7 -0
- package/docs/module-api-changelog.md +9 -0
- package/docs/page-readings.json +963 -992
- package/modules/autonomy/routes/autonomy.js +14 -5
- package/modules/government/decision-board.js +13 -3
- package/modules/government/decision-rules.js +61 -13
- package/modules/lifecycle/routes/done-when.js +2 -2
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/modules/lifecycle/routes/goals.js +4 -4
- package/modules/lifecycle/routes/project-settings.js +2 -2
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/modules/lifecycle/routes/versions.js +1 -1
- package/modules/npm-release/routes/release.js +1 -0
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/provisioning/github-setup.js +129 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/provisioning.js +3 -0
- package/modules/provisioning/routes/callback-page.js +66 -1
- package/modules/provisioning/routes/core-upgrade.js +11 -0
- package/modules/provisioning/routes/github-setup.js +113 -0
- package/modules/provisioning/routes/provisioning.js +11 -45
- package/modules/public-landing/public/projects.html +105 -57
- package/modules/render-deploy/routes/act.js +1 -1
- package/modules/security/routes/reports.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/fitness.js +7 -0
- package/scripts/gds/oauth-secret.js +4 -2
- package/scripts/gds/provision-github-setup.js +148 -0
- package/scripts/gds/provision.js +19 -5
- package/src/bongos/act-alone.js +84 -0
- package/src/bongos/routes/builders.js +6 -1
- package/src/module-api.js +7 -1
- package/tests/autonomy_fence_priority.mjs +3 -1
- package/tests/github_one_trip.mjs +297 -0
- package/tests/government_act_alone_buildings.mjs +430 -0
- package/tests/module_api.mjs +1 -0
- package/tests/npm_release_release.mjs +2 -1
- package/tests/projects_hub_dns_ready.mjs +2 -1
- package/tests/projects_hub_pre_uat.mjs +17 -9
- package/tests/provision.mjs +51 -9
- package/tests/provisioning_github_app_routes.mjs +94 -8
- package/tests/ship_mode.mjs +2 -1
- package/tests/wizard_github_feedback.mjs +8 -5
|
@@ -39,8 +39,18 @@ branding.branding = () => ({ ...realBranding(), identity: { ...(realBranding().i
|
|
|
39
39
|
const api = require(`${ROOT}/src/module-api.js`);
|
|
40
40
|
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
41
41
|
|
|
42
|
+
// The verified-facts read (task 1004649) is the one DB query the github-setup routes make;
|
|
43
|
+
// stub it on the cached module like the data-access fns above.
|
|
44
|
+
const githubSetup = require(`${ROOT}/modules/provisioning/github-setup.js`);
|
|
45
|
+
let facts = {};
|
|
46
|
+
const reported = [];
|
|
47
|
+
githubSetup.githubSetupFacts = async () => facts;
|
|
48
|
+
const prevRecord = provisioning.recordEvent;
|
|
49
|
+
provisioning.recordEvent = async (_db, e) => { reported.push(e); return prevRecord(); };
|
|
50
|
+
|
|
42
51
|
const express = require('express');
|
|
43
52
|
const provisioningRoutes = require(`${ROOT}/modules/provisioning/routes/provisioning.js`);
|
|
53
|
+
const githubSetupRoutes = require(`${ROOT}/modules/provisioning/routes/github-setup.js`);
|
|
44
54
|
const app = express();
|
|
45
55
|
app.use(express.json());
|
|
46
56
|
// res.fail is mounted globally by the core error middleware in production; add a minimal
|
|
@@ -53,6 +63,7 @@ app.use((_req, res, next) => {
|
|
|
53
63
|
next();
|
|
54
64
|
});
|
|
55
65
|
app.use('/api/bongos', provisioningRoutes());
|
|
66
|
+
app.use('/api/bongos', githubSetupRoutes());
|
|
56
67
|
|
|
57
68
|
const server = app.listen(0);
|
|
58
69
|
await new Promise((r) => server.once('listening', r));
|
|
@@ -70,6 +81,7 @@ console.log('POST /provisioning/instances/:id/github-app (mint the one-click lin
|
|
|
70
81
|
t('manifest.redirect_url is the MOTHERSHIP callback', j.manifest && j.manifest.redirect_url === 'https://cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
|
|
71
82
|
t('manifest.callback_urls are the INSTANCE sign-in (both prefixes)', j.manifest && j.manifest.callback_urls[0] === 'https://mercury.example.com/api/gds/auth/web/callback' && j.manifest.callback_urls[1] === 'https://mercury.example.com/api/bongos/auth/web/callback');
|
|
72
83
|
t('manifest.name is the instance domain (unique + matches the consent-screen redirect) within 34 chars', j.manifest && j.manifest.name === 'mercury.example.com' && j.manifest.name.length <= 34, j.manifest && j.manifest.name);
|
|
84
|
+
t('manifest.setup_url sends an install back to the MOTHERSHIP setup route (task 1004649)', j.manifest && j.manifest.setup_url === 'https://cloudbongos.com/api/bongos/provisioning/github-app/setup' && j.manifest.setup_on_update === true, j.manifest && j.manifest.setup_url);
|
|
73
85
|
t('org_url derived from the target_ref owner (acme)', j.org_url === `https://github.com/organizations/acme/settings/apps/new?state=${j.state}`, j.org_url);
|
|
74
86
|
t('the attempt row was persisted with the minted state', !!(stored && stored.instanceId === 42 && stored.state === j.state && stored.requestedBy === 7));
|
|
75
87
|
}
|
|
@@ -93,15 +105,83 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
93
105
|
{
|
|
94
106
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
|
|
95
107
|
const html = await res.text();
|
|
96
|
-
t('good state → 200 text/html
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
t('carries a plain "back to my checklist" link (works without script)', /<a class="btn" id="back" href="\/projects\?view=new">Back to my checklist/.test(html));
|
|
108
|
+
t('good state → 200 text/html one-trip page', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /GitHub app created/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
|
|
109
|
+
// task 1004649: GitHub's half of the approval is done — the SAME tab goes on to the install
|
|
110
|
+
t('says the next step is installing it on the repo', /Next, install it on your repo/.test(html));
|
|
111
|
+
t('carries a plain way back to the project (works without script)', /<a class="btn" id="back" href="\/projects\?view=new">Back to my project/.test(html));
|
|
101
112
|
t('no progress stepper or countdown left on the page', !/id="steps"/.test(html) && !/Placing credentials/.test(html) && !/location\.href=target/.test(html));
|
|
102
113
|
|
|
103
|
-
// Run the served script
|
|
104
|
-
|
|
114
|
+
// Run the served INSTALL script (not a copy): it polls the real status shape and goes on.
|
|
115
|
+
const installScript = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
|
|
116
|
+
t('the one-trip page ships its client script', typeof installScript === 'string' && /installations|install_url/.test(installScript));
|
|
117
|
+
async function runInstall(statuses, repoAnswer) {
|
|
118
|
+
const vm = await import('node:vm');
|
|
119
|
+
const back = { href: '/projects?view=new' }, note = { textContent: '' };
|
|
120
|
+
const timers = [], fetched = [];
|
|
121
|
+
const calls = { replaced: null };
|
|
122
|
+
const sandbox = {
|
|
123
|
+
__CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com', back: '/projects?view=new' },
|
|
124
|
+
document: { getElementById: (id) => (id === 'back' ? back : id === 'note' ? note : null) },
|
|
125
|
+
location: { replace: (u) => { calls.replaced = u; } },
|
|
126
|
+
setTimeout: (fn) => { timers.push(fn); return timers.length; },
|
|
127
|
+
encodeURIComponent, String,
|
|
128
|
+
fetch: async (url) => {
|
|
129
|
+
fetched.push(url);
|
|
130
|
+
if (/api\.github\.com\/repos\//.test(url)) return repoAnswer ? { ok: true, json: async () => repoAnswer } : { ok: false, json: async () => ({}) };
|
|
131
|
+
const body = statuses.length > 1 ? statuses.shift() : statuses[0];
|
|
132
|
+
return { ok: true, json: async () => body };
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
sandbox.window = sandbox;
|
|
136
|
+
vm.runInNewContext(installScript, sandbox);
|
|
137
|
+
for (let i = 0; i < 20 && !calls.replaced; i++) {
|
|
138
|
+
await new Promise((r) => setImmediate(r));
|
|
139
|
+
if (!calls.replaced && timers.length) timers.shift()();
|
|
140
|
+
}
|
|
141
|
+
return { ...calls, fetched, note: note.textContent };
|
|
142
|
+
}
|
|
143
|
+
// the REAL status route's answer for this attempt, before and after the runner named the app
|
|
144
|
+
const statusOf = async () => (await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`)).json();
|
|
145
|
+
const prevGet = provisioning.getOAuthManifestByState;
|
|
146
|
+
provisioning.getOAuthManifestByState = async () => ({ id: 99, instance_id: 42, state: 'GOODSTATE', status: 'converting', attempts: 1, expires_at: new Date(Date.now() + 600000).toISOString() });
|
|
147
|
+
const waiting = await statusOf();
|
|
148
|
+
provisioning.getOAuthManifestByState = async () => ({ id: 99, instance_id: 42, state: 'GOODSTATE', status: 'converting', attempts: 1, app_id: 5, app_slug: 'mercury-app', expires_at: new Date(Date.now() + 600000).toISOString() });
|
|
149
|
+
const named = await statusOf();
|
|
150
|
+
provisioning.getOAuthManifestByState = prevGet;
|
|
151
|
+
t('the real status: no install link until the app is named, then the one-trip link with the state', waiting.install_url === null && named.install_url === 'https://github.com/apps/mercury-app/installations/new?state=GOODSTATE' && named.repo === 'acme/mercury', JSON.stringify({ waiting: waiting.install_url, named: named.install_url }));
|
|
152
|
+
const narrowed = await runInstall([waiting, named], { id: 777, owner: { id: 42424 } });
|
|
153
|
+
t('waits for the app, then sends the founder to install it on THEIR repo, preselected', narrowed.replaced === 'https://github.com/apps/mercury-app/installations/new/permissions?state=GOODSTATE&suggested_target_id=42424&repository_ids[]=777', JSON.stringify(narrowed));
|
|
154
|
+
const plain = await runInstall([named], null);
|
|
155
|
+
t('a repo GitHub will not describe (private) falls back to the plain install page', plain.replaced === 'https://github.com/apps/mercury-app/installations/new?state=GOODSTATE', JSON.stringify(plain));
|
|
156
|
+
const doneAlready = await runInstall([{ ...named, checklist: { ...named.checklist, install: 'done' } }], null);
|
|
157
|
+
t('already installed → straight back to the checklist', doneAlready.replaced === '/projects?view=new#ghstate=GOODSTATE', JSON.stringify(doneAlready));
|
|
158
|
+
const stopped = await runInstall([{ ...named, status: 'error' }], null);
|
|
159
|
+
t('a stopped attempt → back to the checklist, which says what stopped', stopped.replaced === '/projects?view=new#ghstate=GOODSTATE', JSON.stringify(stopped));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log('\nGET /provisioning/github-app/setup (the App\'s setup_url — task 1004649):');
|
|
163
|
+
{
|
|
164
|
+
reported.length = 0;
|
|
165
|
+
const prevGet = provisioning.getOAuthManifestByState;
|
|
166
|
+
provisioning.getOAuthManifestByState = async (_db, s) => (s === 'abcdef0123456789abcdef' ? { id: 99, instance_id: 42, requested_by: 7, state: s, status: 'done', app_slug: 'mercury-app' } : null);
|
|
167
|
+
const res = await fetch(`${base}/provisioning/github-app/setup?installation_id=123456&setup_action=install&state=abcdef0123456789abcdef`);
|
|
168
|
+
const html = await res.text();
|
|
169
|
+
t('a known state → 200 hand-back page to the checklist, carrying the attempt', res.status === 200 && /Installed on GitHub/.test(html) && html.includes('abcdef0123456789abcdef') && /Back to my checklist/.test(html), `status=${res.status}`);
|
|
170
|
+
t('records only that an install was REPORTED (the id is unverified), for the runner to check', reported.length === 1 && reported[0].event === 'github-app-install-reported' && /installation 123456, unverified/.test(reported[0].detail), JSON.stringify(reported));
|
|
171
|
+
reported.length = 0;
|
|
172
|
+
const forged = await fetch(`${base}/provisioning/github-app/setup?installation_id=1;rm%20-rf&state=abcdef0123456789abcdef`);
|
|
173
|
+
t('a non-numeric installation id never reaches the record', forged.status === 200 && reported.length === 1 && !/rm/.test(reported[0].detail), JSON.stringify(reported));
|
|
174
|
+
reported.length = 0;
|
|
175
|
+
const unknown = await fetch(`${base}/provisioning/github-app/setup?installation_id=1&state=NOPE`);
|
|
176
|
+
const uh = await unknown.text();
|
|
177
|
+
t('an unknown or missing state → the same way back, nothing recorded, no oracle', unknown.status === 200 && /Back to my projects/.test(uh) && reported.length === 0, `status=${unknown.status}`);
|
|
178
|
+
provisioning.getOAuthManifestByState = prevGet;
|
|
179
|
+
}
|
|
180
|
+
{
|
|
181
|
+
// the plain hand-back page (now the setup_url's) still behaves as before (task 1004548)
|
|
182
|
+
const callbackPage = require(`${ROOT}/modules/provisioning/routes/callback-page.js`).callbackPage;
|
|
183
|
+
const html = callbackPage({ ok: true, title: 'Installed on GitHub', state: 'GOODSTATE', domain: 'mercury.example.com' });
|
|
184
|
+
t('carries a plain "back to my checklist" link (works without script)', /<a class="btn" id="back" href="\/projects\?view=new">Back to my checklist/.test(html));
|
|
105
185
|
const script = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
|
|
106
186
|
t('the hand-back page ships the client script', typeof script === 'string' && script.length > 100);
|
|
107
187
|
async function runCallback({ opener }) {
|
|
@@ -155,7 +235,13 @@ console.log('\nGET /provisioning/github-app/status (the page poll → auto-open
|
|
|
155
235
|
: null);
|
|
156
236
|
const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
|
|
157
237
|
const j = await res.json();
|
|
158
|
-
|
|
238
|
+
// task 1004649: sign-in verified is not the end of the trip — the install still is ahead
|
|
239
|
+
t("status 'done', install not verified → ready:true, NOT terminal, the install still ahead", res.status === 200 && j.ready === true && j.terminal === false && j.checklist && j.checklist.sign_in === 'done' && j.checklist.install === 'todo', JSON.stringify(j));
|
|
240
|
+
facts = { installedAt: new Date().toISOString() };
|
|
241
|
+
provisioning.getOAuthManifestByState = async () => ({ id: 99, instance_id: 42, state: 'GOODSTATE', status: 'done', app_slug: 'mercury-app', resolved_at: new Date(Date.now() - 3 * 3600 * 1000).toISOString(), expires_at: new Date().toISOString() });
|
|
242
|
+
const j2 = await (await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`)).json();
|
|
243
|
+
t('installed + sign-in verified, the runner done checking → connected, terminal, no install link', j2.checklist.connected === true && j2.checklist.device_flow === 'later' && j2.terminal === true && j2.install_url === null, JSON.stringify(j2));
|
|
244
|
+
facts = {};
|
|
159
245
|
provisioning.getOAuthManifestByState = prev;
|
|
160
246
|
}
|
|
161
247
|
{
|
package/tests/ship_mode.mjs
CHANGED
|
@@ -114,7 +114,8 @@ test('GET /project-settings/ship-mode is open to any signed-in builder, not gate
|
|
|
114
114
|
const names = route('get', '/project-settings/ship-mode').stack.map((l) => l.name);
|
|
115
115
|
assert.equal(names.length, 2, 'requireBuilder + the handler, and no permission gate');
|
|
116
116
|
const write = route('patch', '/project-settings/ship-mode').stack;
|
|
117
|
-
assert.equal(write.length,
|
|
117
|
+
assert.equal(write.length, 4, 'the write carries requireBuilder + the settings permission + Rules\' decision rule (task 1004657) + the handler');
|
|
118
|
+
assert.equal(write[2].handle.name, 'actAloneGate');
|
|
118
119
|
} finally { restore(); }
|
|
119
120
|
});
|
|
120
121
|
|
|
@@ -69,7 +69,8 @@ test('publicGithubApp: effective status, failure only on error, started_at, and
|
|
|
69
69
|
|
|
70
70
|
// ── 2. the words for each stage ─────────────────────────────────────────────
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
// from the shared helpers of the four GitHub steps (task 1004649) to the next step
|
|
73
|
+
const RS = SCRIPT.indexOf(' /* The four GitHub steps of part 03');
|
|
73
74
|
const RE = SCRIPT.indexOf(' /* device-flow', RS);
|
|
74
75
|
const RENDER = SCRIPT.slice(RS, RE);
|
|
75
76
|
|
|
@@ -80,7 +81,8 @@ test('the GitHub step region is still findable (a rename must fail loudly)', ()
|
|
|
80
81
|
|
|
81
82
|
function stage(live, { stale = false } = {}) {
|
|
82
83
|
const sandbox = {
|
|
83
|
-
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, instStatus: 'active', addrReady: true, ...live },
|
|
84
|
+
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null, instStatus: 'active', addrReady: true, ...live },
|
|
85
|
+
state: { name: 'Mercury', slug: 'mercury' },
|
|
84
86
|
manifestStale: stale,
|
|
85
87
|
autoOpenArmed: false,
|
|
86
88
|
resolvedDomain: () => 'mercury.example.com',
|
|
@@ -109,7 +111,8 @@ test('pending: waiting for approval, and says where a missing GitHub tab went',
|
|
|
109
111
|
test('converting is the SAME stage as code_received — approved, placing credentials — never "waiting for approval"', () => {
|
|
110
112
|
for (const m of ['code_received', 'converting']) {
|
|
111
113
|
const html = stage({ manifest: m });
|
|
112
|
-
assert.match(html, /
|
|
114
|
+
assert.match(html, /Approved on GitHub/, m);
|
|
115
|
+
assert.match(html, /goes on to installing it on your repo/, `${m}: the trip carries on to the install (task 1004649)`);
|
|
113
116
|
assert.match(html, /<prog now=1 done=1/, `${m}: the strip is past the approval stage`);
|
|
114
117
|
assert.doesNotMatch(html, /Waiting for your approval/, m);
|
|
115
118
|
}
|
|
@@ -161,8 +164,8 @@ const SEED = SCRIPT.slice(SS, SE);
|
|
|
161
164
|
test('the reload regions are still findable (a rename must fail loudly)', () => {
|
|
162
165
|
assert.ok(WS > 0 && WE > WS, 'wait-timer boundaries found');
|
|
163
166
|
assert.ok(SS > 0 && SE > SS, 'instance-read seed boundaries found');
|
|
164
|
-
assert.match(SCRIPT, /if \(!terminal \|\| manifestInFlight\(\)\) pollTimer = setTimeout\(tick, 5000\);/,
|
|
165
|
-
'a GitHub leg in flight keeps the instance read alive once the project is up
|
|
167
|
+
assert.match(SCRIPT, /if \(!terminal \|\| manifestInFlight\(\) \|\| setupOpen\(\)\) pollTimer = setTimeout\(tick, 5000\);/,
|
|
168
|
+
'a GitHub leg in flight — or an install not yet verified (task 1004649) — keeps the instance read alive once the project is up');
|
|
166
169
|
});
|
|
167
170
|
|
|
168
171
|
// A page that RELOADED: no click in this tab, no manifest poll, just the instance read.
|