@bongos/core 1.19.82 → 1.19.84

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.
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/publish-credential-check.js — can the publish credential SEE Actions?
3
+ // (task 1002733, promoted from idea 1000705.)
4
+ //
5
+ // Run ON THE HOST, where GITHUB_PUSH_TOKEN / the GitHub App env lives:
6
+ // node scripts/gds/publish-credential-check.js
7
+ //
8
+ // WHY. On 2026-08-12 task 1002695 stranded at 'confirmed' across four reconciler
9
+ // sweeps: its PR was MERGED with green checks, but the server's deploy probe kept
10
+ // answering 'pending' because the credential could not read the Actions API. That
11
+ // blindness was invisible — the only way to find it was to notice a strand and
12
+ // hand-run getPublishStatus against the live server. This turns a silent
13
+ // misconfiguration into a one-command answer.
14
+ //
15
+ // Task 1002724 stopped the blindness from stranding work (a blind probe no longer
16
+ // vetoes a git-history-proven land). This tells you whether you are relying on that
17
+ // fallback instead of a real deploy gate.
18
+ //
19
+ // Exit codes: 0 = credential can read Actions (gate is real, or the workflow is
20
+ // legitimately absent), 1 = BLIND (remediation printed), 2 = nothing to check.
21
+
22
+ const githubPush = require('../../modules/lifecycle/github-push');
23
+ const { cliExit } = require('./cli-lib');
24
+
25
+ const C = process.stdout.isTTY
26
+ ? { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', b: '\x1b[1m', x: '\x1b[0m' }
27
+ : { g: '', y: '', r: '', b: '', x: '' };
28
+
29
+ (async () => {
30
+ if (!githubPush.isConfigured()) {
31
+ console.log(`${C.y}!${C.x} No publish credential configured on this machine.`);
32
+ console.log(' This check belongs on the server/host that holds GITHUB_PUSH_TOKEN or the GitHub App env.');
33
+ return cliExit(2);
34
+ }
35
+
36
+ let probe;
37
+ try {
38
+ probe = await githubPush.probeDeployVisibility();
39
+ } catch (e) {
40
+ console.log(`${C.r}✖${C.x} probe failed: ${e && e.message}`);
41
+ return cliExit(1);
42
+ }
43
+
44
+ const http = probe.http ? ` (HTTP ${probe.http})` : '';
45
+ switch (probe.state) {
46
+ case 'readable':
47
+ console.log(`${C.g}✓${C.x} ${C.b}Actions readable${C.x}${http} — ${probe.detail}`);
48
+ return cliExit(0);
49
+ case 'readable_no_workflow':
50
+ console.log(`${C.g}✓${C.x} ${C.b}Actions readable${C.x}${http} — ${probe.detail}`);
51
+ return cliExit(0);
52
+ case 'blind':
53
+ console.log(`${C.r}✖${C.x} ${C.b}BLIND${C.x}${http} — ${probe.detail}`);
54
+ console.log('');
55
+ console.log(` ${C.b}Fix:${C.x} ${probe.remediation}`);
56
+ console.log(' Until then the reconciler falls back to the git-history land proof (task 1002724),');
57
+ console.log(' so work still lands — but the deploy gate is not actually being enforced.');
58
+ return cliExit(1);
59
+ default:
60
+ console.log(`${C.y}!${C.x} ${probe.state}${http} — ${probe.detail}`);
61
+ return cliExit(2);
62
+ }
63
+ })();
package/src/module-api.js CHANGED
@@ -591,8 +591,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
591
591
  // landed since 1.19.80 with no explicit bump. run 31649705242. (task 1002620)
592
592
  // 1.19.82 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
593
593
  // landed since 1.19.81 with no explicit bump. run 31650108283. (task 1002620)
594
+ // 1.19.83 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
595
+ // landed since 1.19.82 with no explicit bump. run 31651046759. (task 1002620)
596
+ // 1.19.84 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
597
+ // landed since 1.19.83 with no explicit bump. run 31651217970. (task 1002620)
594
598
  // ---------------------------------------------------------------------------
595
- const CORE_VERSION = '1.19.82'; // CI auto-patch carrier (ADR 0161); see changelog above
599
+ const CORE_VERSION = '1.19.84'; // CI auto-patch carrier (ADR 0161); see changelog above
596
600
 
597
601
  // A namespaced logger so a module's log lines are attributable + consistent.
598
602
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -570,5 +570,74 @@ await t('landConfirmed: unconfigured deploy counts as landed, but ONLY when merg
570
570
  assert.equal(gp.landConfirmed({ merged: true, deploy_status: 'none' }), false, 'none ≠ unconfigured — no PR at all is not a land');
571
571
  });
572
572
 
573
+ // ===========================================================================
574
+ // task 1002733 — probeDeployVisibility: is the publish credential BLIND?
575
+ //
576
+ // On a private repo GitHub deliberately blurs "not there" and "not yours" — both
577
+ // can surface as 404 — so one call cannot tell "this repo has no deploy-prod.yml"
578
+ // (fine) from "this credential cannot read Actions" (the silent misconfiguration
579
+ // that stranded task 1002695 across 4 sweeps). The CONTROL call is listing the
580
+ // repo's workflows: if that works, a 404 on the specific workflow is genuine.
581
+ // ===========================================================================
582
+
583
+ await t('probeDeployVisibility: workflows list 403 → blind, with remediation', async () => {
584
+ const fetchImpl = async (url) => {
585
+ if (url.includes('/actions/workflows?')) return jsonRes(403, { message: 'Resource not accessible' });
586
+ throw new Error('must not probe the specific workflow once the list is refused: ' + url);
587
+ };
588
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
589
+ assert.equal(p.state, 'blind');
590
+ assert.equal(p.http, 403);
591
+ assert.match(p.remediation, /Actions: read/i);
592
+ // The remediation must name the credential that is ACTUALLY live rather than
593
+ // guessing: resolveToken prefers the App and falls back to GITHUB_PUSH_TOKEN, and an
594
+ // instance runs on exactly one of them. Pointing at the wrong one sends the operator
595
+ // to widen permissions on a credential that is not in play (cloudbongos.com is on
596
+ // the PAT, despite ADR 0055's history saying the App replaced it — task 1002733).
597
+ assert.match(
598
+ p.remediation, /GITHUB_APP_\*|GITHUB_PUSH_TOKEN/,
599
+ 'must identify which credential resolveToken will actually use'
600
+ );
601
+ });
602
+
603
+ await t('probeDeployVisibility: list OK + runs 404 → readable_no_workflow (NOT blind)', async () => {
604
+ const fetchImpl = async (url) => {
605
+ if (url.includes('/actions/workflows?')) return jsonRes(200, { workflows: [{ id: 1, path: '.github/workflows/unit.yml' }] });
606
+ if (url.includes('deploy-prod.yml/runs')) return jsonRes(404, { message: 'Not Found' });
607
+ throw new Error('unexpected ' + url);
608
+ };
609
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
610
+ assert.equal(p.state, 'readable_no_workflow', 'a 404 behind a readable list means the workflow is genuinely absent');
611
+ assert.equal(p.remediation, null, 'nothing to fix — this is the ADR 0108 pin-bump shape');
612
+ });
613
+
614
+ await t('probeDeployVisibility: list OK + runs OK → readable (the deploy gate is real)', async () => {
615
+ const fetchImpl = async (url) => {
616
+ if (url.includes('/actions/workflows?')) return jsonRes(200, { workflows: [] });
617
+ if (url.includes('deploy-prod.yml/runs')) return jsonRes(200, { workflow_runs: [] });
618
+ throw new Error('unexpected ' + url);
619
+ };
620
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
621
+ assert.equal(p.state, 'readable');
622
+ assert.equal(p.remediation, null);
623
+ });
624
+
625
+ await t('probeDeployVisibility: list OK but runs 403 → still blind', async () => {
626
+ const fetchImpl = async (url) => {
627
+ if (url.includes('/actions/workflows?')) return jsonRes(200, { workflows: [] });
628
+ if (url.includes('deploy-prod.yml/runs')) return jsonRes(403, { message: 'nope' });
629
+ throw new Error('unexpected ' + url);
630
+ };
631
+ const p = await gp.probeDeployVisibility({ token: 'TT', repoInfo: REPO, fetchImpl });
632
+ assert.equal(p.state, 'blind');
633
+ assert.equal(p.http, 403);
634
+ });
635
+
636
+ await t('probeDeployVisibility: no credential → no_credential, not a false alarm', async () => {
637
+ const p = await gp.probeDeployVisibility({ token: null, repoInfo: REPO, fetchImpl: async () => { throw new Error('must not fetch'); } });
638
+ assert.equal(p.state, 'no_credential');
639
+ assert.equal(p.remediation, null);
640
+ });
641
+
573
642
  console.log(`\n${pass} passed, ${fail} failed`);
574
643
  process.exit(fail === 0 ? 0 : 1);