@bongos/core 1.21.130 → 1.21.132
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 +14 -14
- package/docs/module-api-changelog.md +4 -0
- package/modules/provisioning/github-setup.js +2 -2
- package/modules/provisioning/pollers/liveness-sweep.js +29 -14
- package/modules/provisioning/tests/liveness.mjs +6 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/github_sign_in_seen.mjs +102 -14
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.132",
|
|
6
|
+
"core_contract": "1.21.132",
|
|
7
|
+
"source_commit": "10dc21ed515c04c95e9dba666c809be9ea45a593",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-15T18:14:12.382Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 606,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 3781,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "2f366274bb2738a2385ff8682e82cddcda6f1cffea387e43a5a9001755b9ee84",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2922,7 +2922,7 @@
|
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "docs/module-api-changelog.md",
|
|
2924
2924
|
"mode": "0000644",
|
|
2925
|
-
"sha256": "
|
|
2925
|
+
"sha256": "fcac7f752834fcc7244b69e173b1c9711bb86100e1904e3a59933fa508716c0c"
|
|
2926
2926
|
},
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/modules-contract.md",
|
|
@@ -8567,7 +8567,7 @@
|
|
|
8567
8567
|
{
|
|
8568
8568
|
"path": "modules/provisioning/github-setup.js",
|
|
8569
8569
|
"mode": "0000644",
|
|
8570
|
-
"sha256": "
|
|
8570
|
+
"sha256": "ba6b384d7a7a73bd38f0db9f1d93bd02ebd87c60014bfbd7494bf4a9010c5538"
|
|
8571
8571
|
},
|
|
8572
8572
|
{
|
|
8573
8573
|
"path": "modules/provisioning/look.js",
|
|
@@ -8802,7 +8802,7 @@
|
|
|
8802
8802
|
{
|
|
8803
8803
|
"path": "modules/provisioning/pollers/liveness-sweep.js",
|
|
8804
8804
|
"mode": "0000644",
|
|
8805
|
-
"sha256": "
|
|
8805
|
+
"sha256": "18578b6eafb69daf2991323ecc2aebf668e1209fc500ff30c165aca2b1bfcfd4"
|
|
8806
8806
|
},
|
|
8807
8807
|
{
|
|
8808
8808
|
"path": "modules/provisioning/pollers/runner-drift-notify.js",
|
|
@@ -8942,7 +8942,7 @@
|
|
|
8942
8942
|
{
|
|
8943
8943
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
8944
8944
|
"mode": "0000644",
|
|
8945
|
-
"sha256": "
|
|
8945
|
+
"sha256": "f4adaf4dacf05b31f7848c62fafafc3f0498e276b2f4fe4d04f18ee5b27fd969"
|
|
8946
8946
|
},
|
|
8947
8947
|
{
|
|
8948
8948
|
"path": "modules/provisioning/tests/provisioning.mjs",
|
|
@@ -10502,12 +10502,12 @@
|
|
|
10502
10502
|
{
|
|
10503
10503
|
"path": "package-lock.json",
|
|
10504
10504
|
"mode": "0000644",
|
|
10505
|
-
"sha256": "
|
|
10505
|
+
"sha256": "6664906b88a5d77b193eb6c60bcad81603678fd33b14dc6c60d94e56cd7221d0"
|
|
10506
10506
|
},
|
|
10507
10507
|
{
|
|
10508
10508
|
"path": "package.json",
|
|
10509
10509
|
"mode": "0000644",
|
|
10510
|
-
"sha256": "
|
|
10510
|
+
"sha256": "971b67f78304f94f08f4ca1f66bab6b692362b81b949aa8ed886e51696000d83"
|
|
10511
10511
|
},
|
|
10512
10512
|
{
|
|
10513
10513
|
"path": "public-docs/index.html",
|
|
@@ -10527,7 +10527,7 @@
|
|
|
10527
10527
|
{
|
|
10528
10528
|
"path": "release-notes.json",
|
|
10529
10529
|
"mode": "0000644",
|
|
10530
|
-
"sha256": "
|
|
10530
|
+
"sha256": "ba6b8f8a08e0aaf99bb72acb3f28f291bc92232c5f234e4c7ab86ca7c23bcb95"
|
|
10531
10531
|
},
|
|
10532
10532
|
{
|
|
10533
10533
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -12882,7 +12882,7 @@
|
|
|
12882
12882
|
{
|
|
12883
12883
|
"path": "src/module-api.js",
|
|
12884
12884
|
"mode": "0000644",
|
|
12885
|
-
"sha256": "
|
|
12885
|
+
"sha256": "551446e38657afe5ad3430c5b33ee82f052bee8d6da7f4ad24f4ffeef24d57dd"
|
|
12886
12886
|
},
|
|
12887
12887
|
{
|
|
12888
12888
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14617,7 +14617,7 @@
|
|
|
14617
14617
|
{
|
|
14618
14618
|
"path": "tests/github_sign_in_seen.mjs",
|
|
14619
14619
|
"mode": "0000644",
|
|
14620
|
-
"sha256": "
|
|
14620
|
+
"sha256": "6c17ca54ca78aadfd0032c91f28d642740f2489a8baa94b5958c64be5276a492"
|
|
14621
14621
|
},
|
|
14622
14622
|
{
|
|
14623
14623
|
"path": "tests/github_token_session_life.mjs",
|
|
@@ -3097,5 +3097,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3097
3097
|
landed since 1.21.128 with no explicit bump. run 38773346540. (task 1002620)
|
|
3098
3098
|
1.21.130 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3099
3099
|
landed since 1.21.129 with no explicit bump. run 38775541138. (task 1002620)
|
|
3100
|
+
1.21.131 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3101
|
+
landed since 1.21.130 with no explicit bump. run 38779214079. (task 1002620)
|
|
3102
|
+
1.21.132 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3103
|
+
landed since 1.21.131 with no explicit bump. run 38783817499. (task 1002620)
|
|
3100
3104
|
---------------------------------------------------------------------------
|
|
3101
3105
|
```
|
|
@@ -47,8 +47,8 @@ const SETUP_EVENTS = Object.freeze({
|
|
|
47
47
|
// the founder pressed "Pick up where it stopped" or "Check again" (task 1004652). Like an
|
|
48
48
|
// install report it re-opens the runner's checking window; it claims nothing itself.
|
|
49
49
|
checkRequested: 'github-app-check-requested',
|
|
50
|
-
// the liveness sweep saw the project's own /healthz answer auth_configured: true
|
|
51
|
-
// 1004410). Recorded once per attempt (recordSignInSeen); no detail.
|
|
50
|
+
// the liveness sweep saw the project's own /api/bongos/healthz answer auth_configured: true
|
|
51
|
+
// (task 1004410). Recorded once per attempt (recordSignInSeen); no detail.
|
|
52
52
|
signInSeen: 'github-sign-in-seen',
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -164,13 +164,20 @@ function probeReason(err) {
|
|
|
164
164
|
return err.message ? String(err.message).slice(0, 40) : 'error';
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// The
|
|
167
|
+
// The health answer is tiny; read no more than this of a body a builder's host chose.
|
|
168
168
|
const BODY_CAP_BYTES = 4096;
|
|
169
169
|
|
|
170
|
+
// auth_configured lives on the JSON health answer; the root /healthz says a plain "ok" and
|
|
171
|
+
// stays the up/down read, so a core without this route never reads as down (task 1004743).
|
|
172
|
+
const AUTH_HEALTHZ_PATH = '/api/bongos/healthz';
|
|
173
|
+
|
|
174
|
+
const is2xx = (res) => res.status >= 200 && res.status < 300;
|
|
175
|
+
|
|
170
176
|
// Did the answer say auth_configured: true (task 1004410)? Reads at most BODY_CAP_BYTES
|
|
171
|
-
// and answers false for anything else — a big, slow or non-JSON body is never "configured".
|
|
177
|
+
// and answers false for anything else — a non-2xx, big, slow or non-JSON body is never "configured".
|
|
172
178
|
async function readAuthConfigured(res) {
|
|
173
179
|
try {
|
|
180
|
+
if (!is2xx(res)) return false;
|
|
174
181
|
if (!res.body || typeof res.body.getReader !== 'function') {
|
|
175
182
|
return typeof res.json === 'function' ? (await res.json()).auth_configured === true : false;
|
|
176
183
|
}
|
|
@@ -192,22 +199,30 @@ async function readAuthConfigured(res) {
|
|
|
192
199
|
|
|
193
200
|
// Knock once. Any 2xx = up; everything else (non-2xx, refused, DNS, TLS, timeout)
|
|
194
201
|
// = down with a reason. Never throws — a probe result is the whole point. An up answer
|
|
195
|
-
// also carries whether the project says its sign-in is configured (task 1004410)
|
|
202
|
+
// also carries whether the project says its sign-in is configured (task 1004410), asked
|
|
203
|
+
// on the same origin through the same pin and time budget; that read failing is only "no".
|
|
196
204
|
async function probeHealthz(url, { timeoutMs = PROBE_TIMEOUT_MS, fetchImpl, pin } = {}) {
|
|
197
205
|
const doFetch = fetchImpl || globalThis.fetch;
|
|
198
206
|
const dispatcher = pin && !fetchImpl ? pinnedDispatcher(pin, timeoutMs) : null;
|
|
207
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
208
|
+
const get = async (target) => doFetch(target, {
|
|
209
|
+
method: 'GET',
|
|
210
|
+
// manual: a redirect must never re-aim the probe (SSRF pivot) — and a
|
|
211
|
+
// healthz that redirects isn't answering anyway.
|
|
212
|
+
redirect: 'manual',
|
|
213
|
+
signal,
|
|
214
|
+
headers: { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' },
|
|
215
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
216
|
+
});
|
|
199
217
|
try {
|
|
200
|
-
const res = await
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
});
|
|
209
|
-
if (res.status >= 200 && res.status < 300) return { up: true, authConfigured: await readAuthConfigured(res) };
|
|
210
|
-
return { up: false, reason: `http ${res.status}` };
|
|
218
|
+
const res = await get(url);
|
|
219
|
+
if (!is2xx(res)) return { up: false, reason: `http ${res.status}` };
|
|
220
|
+
if (res.body && typeof res.body.cancel === 'function') res.body.cancel().catch(() => {});
|
|
221
|
+
// An https answer with no pin reached an address the gate never vetted; ask it nothing more.
|
|
222
|
+
const askAuth = !!pin || !url.startsWith('https://');
|
|
223
|
+
const authConfigured = askAuth
|
|
224
|
+
&& await get(new URL(AUTH_HEALTHZ_PATH, url).href).then(readAuthConfigured, () => false);
|
|
225
|
+
return { up: true, authConfigured };
|
|
211
226
|
} catch (e) {
|
|
212
227
|
return { up: false, reason: probeReason(e) };
|
|
213
228
|
} finally {
|
|
@@ -115,7 +115,7 @@ console.log('\nsweepOnce (real loopback probes, recording fake db):');
|
|
|
115
115
|
|
|
116
116
|
await ta('an answering instance is UP and a refusing one is DOWN — both columns written, status never touched', async () => {
|
|
117
117
|
const server = http.createServer((req, res) => {
|
|
118
|
-
if (req.url === '/healthz') { res.writeHead(200, { 'content-type': '
|
|
118
|
+
if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); res.end('ok'); return; }
|
|
119
119
|
res.writeHead(404); res.end();
|
|
120
120
|
});
|
|
121
121
|
await listen(server);
|
|
@@ -322,15 +322,17 @@ await ta('a rebinding nameserver cannot re-aim the probe — the pinned address
|
|
|
322
322
|
|
|
323
323
|
await ta('the pin is load-bearing on the REAL client: same URL, the pinned address decides where it lands', async () => {
|
|
324
324
|
const server = http.createServer((req, res) => {
|
|
325
|
-
if (req.url === '/healthz') { res.writeHead(200); res.end('
|
|
325
|
+
if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); res.end('ok'); return; }
|
|
326
|
+
if (req.url === '/api/bongos/healthz') { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"ok":true,"auth_configured":true}'); return; }
|
|
326
327
|
res.writeHead(404); res.end();
|
|
327
328
|
});
|
|
328
329
|
await listen(server);
|
|
329
330
|
const url = `http://pinned.invalid:${server.address().port}/healthz`;
|
|
330
331
|
try {
|
|
331
|
-
// 'pinned.invalid' resolves nowhere; only the pin can make this connect
|
|
332
|
+
// 'pinned.invalid' resolves nowhere; only the pin can make this connect — for the sign-in
|
|
333
|
+
// read (task 1004743) as much as for the up/down one.
|
|
332
334
|
const hit = await liveness.probeHealthz(url, { timeoutMs: 3000, pin: { address: '127.0.0.1', family: 4 } });
|
|
333
|
-
assert.deepEqual(hit, { up: true, authConfigured:
|
|
335
|
+
assert.deepEqual(hit, { up: true, authConfigured: true }, 'the pinned address is what both reads connect to');
|
|
334
336
|
const miss = await liveness.probeHealthz(url, { timeoutMs: 3000, pin: { address: '127.0.0.2', family: 4 } });
|
|
335
337
|
assert.equal(miss.up, false, 'a different pin cannot reach the same server — the pin, not DNS, decides');
|
|
336
338
|
} finally {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.132",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.132",
|
|
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.21.
|
|
3
|
+
"version": "1.21.132",
|
|
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
|
@@ -9782,5 +9782,17 @@
|
|
|
9782
9782
|
"id": "1004636",
|
|
9783
9783
|
"text": "People can now say 'show me the how-to', 'what's next in the tutorial' or 'skip the tutorial' and the AI does it: shows the whole guide with what they have seen, gives the next tip, or turns the tips off. 'Start the tutorial o"
|
|
9784
9784
|
}
|
|
9785
|
+
],
|
|
9786
|
+
"1.21.131": [
|
|
9787
|
+
{
|
|
9788
|
+
"id": "1004577",
|
|
9789
|
+
"text": "Two projects whose GitHub sign-in actually worked no longer show it as failed; their setup checklists now point at the real next step."
|
|
9790
|
+
}
|
|
9791
|
+
],
|
|
9792
|
+
"1.21.132": [
|
|
9793
|
+
{
|
|
9794
|
+
"id": "1004743",
|
|
9795
|
+
"text": "A project whose GitHub sign-in already works now gets that step ticked on its setup checklist automatically, within about five minutes."
|
|
9796
|
+
}
|
|
9785
9797
|
]
|
|
9786
9798
|
}
|
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.21.
|
|
78
|
+
const CORE_VERSION = '1.21.132'; // 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');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Sign-in used to tick only from the one-click GitHub attempt reaching 'done', so a project
|
|
6
6
|
// whose sign-in was wired some other way — a hand-made OAuth app, or a GitHub tab that never
|
|
7
7
|
// came back — sat at "Waiting on GitHub" for good. Now the liveness sweep reads the project's
|
|
8
|
-
//
|
|
8
|
+
// JSON health answer, records 'github-sign-in-seen' once per attempt, and githubChecklist reads it.
|
|
9
9
|
// The page half (what the founder sees) is tests/setup_status_facts.mjs §6.
|
|
10
10
|
//
|
|
11
11
|
// Pinned here, DB-free over recording fakes and real loopback servers:
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
// * pickUpFor offers nothing to pick up once sign-in works elsewhere;
|
|
15
15
|
// * githubSetupRead reads the fact even when no attempt exists;
|
|
16
16
|
// * recordSignInSeen writes at most one row per attempt;
|
|
17
|
-
// * probeHealthz reports auth_configured only from a small JSON answer saying true
|
|
17
|
+
// * probeHealthz reports auth_configured only from a small JSON answer saying true, read
|
|
18
|
+
// from <API prefix>/healthz — the root /healthz says a plain "ok" (task 1004743), and every
|
|
19
|
+
// server here answers both paths the way a real project does;
|
|
20
|
+
// * a sign-in read that fails, hangs or redirects never turns an up project down;
|
|
18
21
|
// * sweepOnce records the fact only for an up project that says so.
|
|
19
22
|
//
|
|
20
23
|
// Run: node --test tests/github_sign_in_seen.mjs
|
|
@@ -23,6 +26,7 @@ import { test } from 'node:test';
|
|
|
23
26
|
import assert from 'node:assert/strict';
|
|
24
27
|
import http from 'node:http';
|
|
25
28
|
import path from 'node:path';
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
26
30
|
import { createRequire } from 'node:module';
|
|
27
31
|
import { fileURLToPath } from 'node:url';
|
|
28
32
|
|
|
@@ -30,6 +34,8 @@ const require = createRequire(import.meta.url);
|
|
|
30
34
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
35
|
const gs = require(path.join(ROOT, 'modules', 'provisioning', 'github-setup.js'));
|
|
32
36
|
const liveness = require(path.join(ROOT, 'modules', 'provisioning', 'pollers', 'liveness-sweep.js'));
|
|
37
|
+
const { API_PREFIX } = require(path.join(ROOT, 'src', 'bongos', 'api-prefix.js'));
|
|
38
|
+
const AUTH_PATH = `${API_PREFIX}/healthz`;
|
|
33
39
|
|
|
34
40
|
const SEEN = { signInSeenAt: '2026-10-14T10:00:00.000Z' };
|
|
35
41
|
|
|
@@ -100,13 +106,38 @@ test('recordSignInSeen writes at most one row per attempt, from the liveness swe
|
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
// ── the probe and the sweep ────────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
// A project as it really answers: the ROOT /healthz is a plain-text "ok", and auth_configured
|
|
110
|
+
// lives only on the JSON answer under the API prefix. `api` handles that one path; without it
|
|
111
|
+
// the path 404s, like a core with no such route. A root mocked as JSON is what hid task 1004743.
|
|
112
|
+
function project(api) {
|
|
113
|
+
const server = http.createServer((req, res) => {
|
|
114
|
+
if (req.url === '/healthz') { res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); res.end('ok'); return; }
|
|
115
|
+
if (req.url === AUTH_PATH && api) { api(req, res); return; }
|
|
116
|
+
res.writeHead(404); res.end();
|
|
117
|
+
});
|
|
118
|
+
return listen(server);
|
|
106
119
|
}
|
|
120
|
+
function json(body, status = 200) {
|
|
121
|
+
return (_req, res) => { res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }); res.end(body); };
|
|
122
|
+
}
|
|
123
|
+
function listen(server) { return new Promise((r) => server.listen(0, '127.0.0.1', () => r(server))); }
|
|
107
124
|
function close(server) { if (server.closeAllConnections) server.closeAllConnections(); server.close(); }
|
|
125
|
+
const rootOf = (s) => `http://127.0.0.1:${s.address().port}/healthz`;
|
|
126
|
+
|
|
127
|
+
test('the fixture is the real shape: the root /healthz says a plain-text "ok"', () => {
|
|
128
|
+
const src = readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8').replace(/\r\n/g, '\n');
|
|
129
|
+
assert.match(src, /app\.get\('\/healthz', \(_req, res\) => \{\n\s*res\.type\('text\/plain'\)\.send\('ok'\);/,
|
|
130
|
+
'if the root ever answers JSON, project() above must change with it');
|
|
131
|
+
});
|
|
108
132
|
|
|
109
|
-
test('
|
|
133
|
+
test('a project whose root is the only answer is up, never configured, and never crashes the probe', async () => {
|
|
134
|
+
const s = await project(null);
|
|
135
|
+
try {
|
|
136
|
+
assert.deepEqual(await liveness.probeHealthz(rootOf(s), { timeoutMs: 3000 }), { up: true, authConfigured: false });
|
|
137
|
+
} finally { close(s); }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('probeHealthz says auth_configured only from a small JSON answer under the API prefix saying true', async () => {
|
|
110
141
|
const cases = [
|
|
111
142
|
['{"ok":true,"auth_configured":true}', true],
|
|
112
143
|
['{"ok":true,"auth_configured":false}', false],
|
|
@@ -116,23 +147,80 @@ test('probeHealthz says auth_configured only from a small JSON answer saying tru
|
|
|
116
147
|
[JSON.stringify({ ok: true, auth_configured: true, pad: 'x'.repeat(8000) }), false],
|
|
117
148
|
];
|
|
118
149
|
for (const [body, want] of cases) {
|
|
119
|
-
const s = await
|
|
150
|
+
const s = await project(json(body));
|
|
120
151
|
try {
|
|
121
|
-
const r = await liveness.probeHealthz(
|
|
122
|
-
assert.equal(r.up, true, '
|
|
152
|
+
const r = await liveness.probeHealthz(rootOf(s), { timeoutMs: 3000 });
|
|
153
|
+
assert.equal(r.up, true, 'the root answered, so it is up');
|
|
123
154
|
assert.equal(r.authConfigured, want, body.slice(0, 50));
|
|
124
155
|
} finally { close(s); }
|
|
125
156
|
}
|
|
126
157
|
});
|
|
127
158
|
|
|
159
|
+
test('against the REAL healthz router, the probe reads what the project\'s auth says', async () => {
|
|
160
|
+
const express = require('express');
|
|
161
|
+
const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
|
|
162
|
+
const buildHealthzRouter = require(path.join(ROOT, 'src', 'bongos', 'routes', 'healthz.js'));
|
|
163
|
+
const realAuthConfigured = auth.authConfigured;
|
|
164
|
+
let configured = true;
|
|
165
|
+
let s = null;
|
|
166
|
+
try {
|
|
167
|
+
auth.authConfigured = () => configured;
|
|
168
|
+
const app = express();
|
|
169
|
+
app.get('/healthz', (_req, res) => { res.type('text/plain').send('ok'); });
|
|
170
|
+
app.use(API_PREFIX, buildHealthzRouter());
|
|
171
|
+
s = await listen(http.createServer(app));
|
|
172
|
+
assert.deepEqual(await liveness.probeHealthz(rootOf(s), { timeoutMs: 3000 }), { up: true, authConfigured: true });
|
|
173
|
+
configured = false;
|
|
174
|
+
assert.deepEqual(await liveness.probeHealthz(rootOf(s), { timeoutMs: 3000 }), { up: true, authConfigured: false });
|
|
175
|
+
} finally { auth.authConfigured = realAuthConfigured; if (s) close(s); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('a sign-in read that fails, hangs or redirects never turns an up project down, and is never followed', async () => {
|
|
179
|
+
const elsewhere = await project(json('{"ok":true,"auth_configured":true}'));
|
|
180
|
+
const answers = {
|
|
181
|
+
'a 500': json('{"ok":true,"auth_configured":true}', 500),
|
|
182
|
+
'a hang': () => {},
|
|
183
|
+
'a redirect to a yes': (_req, res) => { res.writeHead(302, { location: `http://127.0.0.1:${elsewhere.address().port}${AUTH_PATH}` }); res.end(); },
|
|
184
|
+
};
|
|
185
|
+
try {
|
|
186
|
+
for (const [name, api] of Object.entries(answers)) {
|
|
187
|
+
const s = await project(api);
|
|
188
|
+
try {
|
|
189
|
+
assert.deepEqual(await liveness.probeHealthz(rootOf(s), { timeoutMs: 400 }), { up: true, authConfigured: false }, name);
|
|
190
|
+
} finally { close(s); }
|
|
191
|
+
}
|
|
192
|
+
} finally { close(elsewhere); }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('the sign-in read rides only a vetted address, and no shape of failure turns it into a down', async () => {
|
|
196
|
+
const asked = [];
|
|
197
|
+
const answering = async (url) => {
|
|
198
|
+
asked.push(String(url));
|
|
199
|
+
return String(url).endsWith(AUTH_PATH) ? new Response('{"ok":true,"auth_configured":true}', { status: 200 }) : new Response('ok', { status: 200 });
|
|
200
|
+
};
|
|
201
|
+
const url = 'https://kestrel.example.com/healthz';
|
|
202
|
+
assert.deepEqual(await liveness.probeHealthz(url, { fetchImpl: answering, pin: { address: '93.184.216.34', family: 4 } }), { up: true, authConfigured: true });
|
|
203
|
+
assert.deepEqual(asked, [url, `https://kestrel.example.com${AUTH_PATH}`], 'the same origin, nowhere else');
|
|
204
|
+
asked.length = 0;
|
|
205
|
+
assert.deepEqual(await liveness.probeHealthz(url, { fetchImpl: answering }), { up: true, authConfigured: false });
|
|
206
|
+
assert.deepEqual(asked, [url], 'an https answer the gate never pinned is asked nothing more');
|
|
207
|
+
|
|
208
|
+
let calls = 0;
|
|
209
|
+
const throwsSecond = (u) => { calls += 1; if (calls > 1) throw new TypeError(`no: ${u}`); return Promise.resolve(new Response('ok', { status: 200 })); };
|
|
210
|
+
assert.deepEqual(await liveness.probeHealthz('http://127.0.0.1:1/healthz', { fetchImpl: throwsSecond }), { up: true, authConfigured: false }, 'a synchronous throw is only "no"');
|
|
211
|
+
const noStatus = await liveness.probeHealthz('http://127.0.0.1:1/healthz', { fetchImpl: async () => ({}) });
|
|
212
|
+
assert.equal(noStatus.up, false, 'an answer with no status is not a 2xx');
|
|
213
|
+
});
|
|
214
|
+
|
|
128
215
|
test('the sweep records the fact for an up project that says so, and for no other', async () => {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
216
|
+
const kestrel = await project(json('{"ok":true,"auth_configured":true}'));
|
|
217
|
+
const tern = await project(json('{"ok":true,"auth_configured":false}'));
|
|
218
|
+
const wren = await project(null);
|
|
131
219
|
const inserts = [];
|
|
132
220
|
const db = {
|
|
133
221
|
query: async (sql, params) => {
|
|
134
222
|
if (/FROM provisioning_instances/.test(sql) && /WHERE status = 'active'/.test(sql)) {
|
|
135
|
-
return { rows: [
|
|
223
|
+
return { rows: [kestrel, tern, wren].map((s, i) => ({ id: i + 1, slug: ['kestrel', 'tern', 'wren'][i], domain: null, port: s.address().port })) };
|
|
136
224
|
}
|
|
137
225
|
if (/^UPDATE provisioning_instances/.test(sql)) return { rows: [{ id: params[0], prev_up: true }] };
|
|
138
226
|
if (/INSERT INTO provisioning_events/.test(sql)) { inserts.push(params); return { rows: [] }; }
|
|
@@ -142,7 +230,7 @@ test('the sweep records the fact for an up project that says so, and for no othe
|
|
|
142
230
|
try {
|
|
143
231
|
liveness.resetNoticeState();
|
|
144
232
|
const out = await liveness.sweepOnce({ pool: db, log: () => {}, timeoutMs: 3000, notify: () => {}, collectRunnerDrift: async () => [] });
|
|
145
|
-
assert.equal(out.up,
|
|
233
|
+
assert.equal(out.up, 3, 'what the sign-in read finds never changes up or down');
|
|
146
234
|
assert.deepEqual(inserts, [[1, 'github-sign-in-seen']]);
|
|
147
|
-
} finally { close(
|
|
235
|
+
} finally { close(kestrel); close(tern); close(wren); }
|
|
148
236
|
});
|