devflow-kit 3.8.0 → 3.9.0
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/CHANGELOG.md +120 -0
- package/README.md +5 -5
- package/dist/agents/code.md +4 -4
- package/dist/agents/fix.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/agents/skim.md +1 -1
- package/dist/agents/triage.md +7 -0
- package/dist/cli/commands/curation.js +67 -16
- package/dist/cli/commands/gate-level-prompts.js +8 -6
- package/dist/cli/commands/gates.js +5 -4
- package/dist/cli/commands/init.js +8 -7
- package/dist/cli/commands/memory.js +59 -12
- package/dist/cli/commands/uninstall.js +77 -9
- package/dist/commands/bug-analysis.md +2 -2
- package/dist/commands/code-review.md +11 -11
- package/dist/commands/debug.md +2 -2
- package/dist/commands/dynamic-build.md +2 -2
- package/dist/commands/dynamic-plan.md +2 -2
- package/dist/commands/dynamic-profile.md +2 -2
- package/dist/commands/dynamic-tickets.md +2 -2
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +2 -2
- package/dist/commands/plan.md +2 -2
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +2 -2
- package/dist/commands/resolve.md +16 -16
- package/dist/commands/self-review.md +2 -2
- package/dist/core/area-pause-notice.js +44 -0
- package/dist/core/curation-move-skip.js +4 -5
- package/dist/core/curation-queue-cleanup.js +6 -5
- package/dist/core/curation-store.js +1 -0
- package/dist/core/evidence-policy.js +10 -8
- package/dist/core/plugins.js +4 -4
- package/dist/core/tracked-areas.js +138 -14
- package/dist/curation-off/agents/code.md +4 -4
- package/dist/curation-off/agents/fix.md +1 -1
- package/dist/curation-off/agents/triage.md +7 -0
- package/dist/curation-off/commands/code-review.md +11 -11
- package/dist/curation-off/commands/debug.md +2 -2
- package/dist/curation-off/commands/dynamic-build.md +2 -2
- package/dist/curation-off/commands/explore.md +2 -2
- package/dist/curation-off/commands/implement.md +2 -2
- package/dist/curation-off/commands/plan.md +2 -2
- package/dist/curation-off/commands/resolve.md +16 -16
- package/dist/curation-off/commands/self-review.md +2 -2
- package/dist/hud/components/curation-counts.js +27 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
- package/dist/skills/git/references/pr/post-review-summary.md +12 -11
- package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
- package/dist/skills/git/references/publication-gate.md +6 -7
- package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
- package/package.json +1 -1
- package/src/assets/agents/code.mds +4 -4
- package/src/assets/agents/evaluate.md +6 -12
- package/src/assets/agents/fix.mds +1 -1
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/skim.mds +1 -1
- package/src/assets/agents/triage.mds +7 -0
- package/src/assets/agents/validate.md +20 -27
- package/src/assets/commands/_partials/_decisions.mds +10 -8
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/code-review.mds +9 -9
- package/src/assets/commands/resolve.mds +14 -14
- package/src/assets/mds/git/_ops.mds +6 -1
- package/src/assets/mds/git/_ops_pr.mds +20 -10
- package/src/assets/mds/git/_ops_steps.mds +10 -0
- package/src/assets/mds/git/_pr.mds +14 -14
- package/src/assets/mds/git/_references.mds +6 -7
- package/src/assets/mds/tracker/_github.mds +1 -4
- package/src/assets/mds/tracker/_jira.mds +1 -4
- package/src/assets/mds/tracker/_linear.mds +1 -4
- package/src/assets/scripts/ci-wait.cjs +389 -92
- package/src/assets/scripts/hooks/background-memory-update +79 -1
- package/src/assets/scripts/hooks/json-helper.cjs +44 -2
- package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
- package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
- package/src/assets/scripts/hooks/memory-permission +10 -2
- package/src/assets/scripts/hooks/memory-worker +11 -0
- package/src/assets/scripts/hooks/pre-compact-memory +11 -0
- package/src/assets/scripts/hooks/queue-append +218 -1
- package/src/assets/scripts/hooks/session-start-context +89 -37
- package/src/assets/scripts/hooks/session-start-memory +39 -0
- package/src/assets/scripts/pr-evidence.cjs +5 -2
- package/src/assets/scripts/resolve-settings.cjs +244 -30
- package/src/assets/scripts/verify-evidence.cjs +307 -75
- package/src/assets/skills/agent-references/SKILL.md +7 -1
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
- package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
- package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
- package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
- package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
|
@@ -75,9 +75,10 @@ const crypto = require('crypto');
|
|
|
75
75
|
const childProcess = require('child_process');
|
|
76
76
|
|
|
77
77
|
const PE = require(path.join(__dirname, 'pr-evidence.cjs'));
|
|
78
|
-
// D-READINESS-CLASSIFIER: the CI
|
|
79
|
-
//
|
|
80
|
-
//
|
|
78
|
+
// D-READINESS-CLASSIFIER: the CI classifier, the merge gate and the workflow-list probe
|
|
79
|
+
// are ci-wait's own, so the two scripts cannot disagree; only ci-wait has the settle
|
|
80
|
+
// window, since readiness makes one read. Its main is guarded by
|
|
81
|
+
// `require.main === module`: requiring it starts nothing.
|
|
81
82
|
const CW = require(path.join(__dirname, 'ci-wait.cjs'));
|
|
82
83
|
|
|
83
84
|
// ---------------------------------------------------------------------------
|
|
@@ -123,11 +124,13 @@ const DEADLINE_MS = 90000;
|
|
|
123
124
|
/**
|
|
124
125
|
* D-PUBLISH-BOUNDS: `publish` runs `verify` under the deadline above, then its writes
|
|
125
126
|
* under a deadline of their own and a gh budget of their own. The writes are at most
|
|
126
|
-
*
|
|
127
|
-
* read-back,
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
127
|
+
* eight calls: two body reads (the compare-and-swap), a body edit and one retry, one
|
|
128
|
+
* read-back, then the comment. A new comment is a post and one retry (seven in all).
|
|
129
|
+
* An edit of the viewer's own comment is the edit, one retry and one read of the
|
|
130
|
+
* comment (eight in all). A scrub is bounded by its own timeout, not by the deadline,
|
|
131
|
+
* so one that starts just before the writes' deadline can end up to REDACT_TIMEOUT_MS
|
|
132
|
+
* after it. The worst case is 90 s + 60 s + 20 s; the caller's 180 s Bash timeout
|
|
133
|
+
* covers it with room for node's startup.
|
|
131
134
|
*/
|
|
132
135
|
const PUBLISH_DEADLINE_MS = 60000;
|
|
133
136
|
const PUBLISH_GH_CALLS = 8;
|
|
@@ -151,9 +154,10 @@ const PR_VIEW_MAX_BUFFER = 64 * 1024 * 1024;
|
|
|
151
154
|
const RUN_LIST_MAX_BUFFER = 1024 * 1024;
|
|
152
155
|
const RUN_VIEW_MAX_BUFFER = 65536;
|
|
153
156
|
const LINE_MAX_BUFFER = 4096;
|
|
157
|
+
/** A comment read back as a JSON string: 65,536 characters, each escaped to six, fit. */
|
|
158
|
+
const COMMENT_READ_MAX_BUFFER = 8 * 65536;
|
|
154
159
|
/** `gh pr checks` and the thread-count GraphQL answer: a thousand rows of names fit. */
|
|
155
160
|
const READINESS_MAX_BUFFER = 4 * 1024 * 1024;
|
|
156
|
-
const READINESS_MAX_CHECKS = 1000;
|
|
157
161
|
/** 5,000 paths of 4,096 bytes overflow this; an overflow reads as an unresolved diff. */
|
|
158
162
|
const DIFF_MAX_BUFFER = 16 * 1024 * 1024;
|
|
159
163
|
const FETCH_MAX_BUFFER = 1024 * 1024;
|
|
@@ -173,6 +177,14 @@ const PR_FIELDS = 'body,comments,reviews,author,headRefOid,baseRefOid,isCrossRep
|
|
|
173
177
|
const PR_RE = /^[1-9][0-9]{0,9}$/;
|
|
174
178
|
const SHA40_RE = /^[0-9a-f]{40}$/;
|
|
175
179
|
|
|
180
|
+
/**
|
|
181
|
+
* A pull-request comment's url, anchored: the pull number, then the numeric id in the
|
|
182
|
+
* fragment. The host is not pinned (GitHub Enterprise Server hosts work). There is no
|
|
183
|
+
* `m` flag, so `$` is the end of the string and a trailing newline is refused.
|
|
184
|
+
*/
|
|
185
|
+
const COMMENT_URL_RE = /^https:\/\/[^/\s?#]+\/[^/\s?#]+\/[^/\s?#]+\/pull\/([1-9][0-9]{0,9})#issuecomment-([1-9][0-9]{0,19})$/;
|
|
186
|
+
/** A comment id as a string of digits: up to 20, so an id beyond 2^53 stays exact. */
|
|
187
|
+
const COMMENT_ID_RE = /^[1-9][0-9]{0,19}$/;
|
|
176
188
|
/** A review state that decides a login's position on the PR (D6). */
|
|
177
189
|
const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
|
|
178
190
|
|
|
@@ -215,7 +227,8 @@ const USAGE = [
|
|
|
215
227
|
* summary, never input bytes. `ghCap` replaces CAPS.GH_CALLS for a stage that
|
|
216
228
|
* carries a budget of its own (`publish`'s writes).
|
|
217
229
|
*
|
|
218
|
-
* @typedef {{ login: string, association: string, body: string, viewerDidAuthor: boolean }} PrComment
|
|
230
|
+
* @typedef {{ login: string, association: string, body: string, viewerDidAuthor: boolean, commentId: string }} PrComment
|
|
231
|
+
* `commentId` is the comment's numeric id as digits, or '' when its url did not parse.
|
|
219
232
|
* @typedef {{ login: string, association: string, state: string }} PrReview
|
|
220
233
|
* @typedef {{ number: number, head: string, base: string, body: string, author: string,
|
|
221
234
|
* isCrossRepository: boolean | undefined, comments: readonly PrComment[],
|
|
@@ -625,6 +638,19 @@ function loginArg(login) {
|
|
|
625
638
|
return login;
|
|
626
639
|
}
|
|
627
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Assert a comment id before it reaches gh's argv (an API path segment):
|
|
643
|
+
* COMMENT_ID_RE, so no `/`, `..`, `?` or option can ride on PR data. commentIdOf
|
|
644
|
+
* only ever yields such ids; this is that precondition held at the sink.
|
|
645
|
+
*
|
|
646
|
+
* @param {string} id
|
|
647
|
+
* @returns {string}
|
|
648
|
+
*/
|
|
649
|
+
function commentIdArg(id) {
|
|
650
|
+
if (typeof id !== 'string' || !COMMENT_ID_RE.test(id)) throw new Error('verify-evidence: unchecked comment id');
|
|
651
|
+
return id;
|
|
652
|
+
}
|
|
653
|
+
|
|
628
654
|
// ---------------------------------------------------------------------------
|
|
629
655
|
// The PR (one gh pr view)
|
|
630
656
|
// ---------------------------------------------------------------------------
|
|
@@ -646,6 +672,21 @@ function loginOf(author) {
|
|
|
646
672
|
return typeof login === 'string' && login.length <= 100 ? login : '';
|
|
647
673
|
}
|
|
648
674
|
|
|
675
|
+
/**
|
|
676
|
+
* The numeric id of a PR comment, read from its url, as the digits themselves:
|
|
677
|
+
* never Number(), so an id beyond 2^53 stays exact. '' unless the url is a bounded
|
|
678
|
+
* string of the anchored pull-comment form AND names the PR that was read.
|
|
679
|
+
*
|
|
680
|
+
* @param {unknown} url
|
|
681
|
+
* @param {number} pr
|
|
682
|
+
* @returns {string}
|
|
683
|
+
*/
|
|
684
|
+
function commentIdOf(url, pr) {
|
|
685
|
+
if (typeof url !== 'string' || url.length > MAX_URL_CHARS) return '';
|
|
686
|
+
const m = COMMENT_URL_RE.exec(url);
|
|
687
|
+
return m !== null && m[1] === String(pr) ? m[2] : '';
|
|
688
|
+
}
|
|
689
|
+
|
|
649
690
|
/**
|
|
650
691
|
* Parse the `gh pr view --json` answer into PrFacts, or null when it is not the
|
|
651
692
|
* PR that was asked for or a field has the wrong shape. Malformed list entries
|
|
@@ -671,6 +712,7 @@ function parsePrJson(json, pr) {
|
|
|
671
712
|
association: typeof x.authorAssociation === 'string' ? x.authorAssociation : '',
|
|
672
713
|
body: typeof x.body === 'string' ? x.body : '',
|
|
673
714
|
viewerDidAuthor: x.viewerDidAuthor === true,
|
|
715
|
+
commentId: commentIdOf(x.url, pr),
|
|
674
716
|
});
|
|
675
717
|
});
|
|
676
718
|
const reviews = o.reviews.slice(-CAPS.REVIEWS).map(r => {
|
|
@@ -751,6 +793,38 @@ function markerComments(pr) {
|
|
|
751
793
|
return pr.comments.filter(c => PE.MARKERS.EVIDENCE_RE.test(firstLine(c.body))).reverse();
|
|
752
794
|
}
|
|
753
795
|
|
|
796
|
+
/**
|
|
797
|
+
* @typedef {{ id: string, marker: string, editable: boolean }} EditTarget
|
|
798
|
+
* `id` is the comment's id ('' when unreadable); `marker` is its first line;
|
|
799
|
+
* `editable` is false when a later comment from another login is itself an
|
|
800
|
+
* evidence marker.
|
|
801
|
+
*/
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* The comment a publish would edit in place: the viewer's NEWEST comment whose first
|
|
805
|
+
* line starts with the evidence head prefix, whatever its key. "The viewer's" is the
|
|
806
|
+
* server-asserted `viewerDidAuthor` flag, so only an own comment is ever a target
|
|
807
|
+
* (never another login's comment, such as a maintainer's or a bot's). A malformed own
|
|
808
|
+
* comment (the prefix alone) is a valid target, so an edit repairs it.
|
|
809
|
+
*
|
|
810
|
+
* `editable` guards the trust order: verify trusts the newest evidence comment by
|
|
811
|
+
* list position, and an edit keeps its old position, so editing is safe only when
|
|
812
|
+
* no strict marker comment from another login comes after the target. A later
|
|
813
|
+
* prefix-only comment from another login is not a record and does not block. The
|
|
814
|
+
* strict trust functions (markerComments, resolveTrust, findTrustedRecord) stay as
|
|
815
|
+
* they are.
|
|
816
|
+
*
|
|
817
|
+
* @param {{ comments: readonly PrComment[] }} pr chronological, newest last
|
|
818
|
+
* @returns {EditTarget | null}
|
|
819
|
+
*/
|
|
820
|
+
function editTarget(pr) {
|
|
821
|
+
const at = pr.comments.findLastIndex(c => c.viewerDidAuthor && firstLine(c.body).startsWith(PE.MARKERS.EVIDENCE_HEAD_PREFIX));
|
|
822
|
+
if (at === -1) return null;
|
|
823
|
+
const c = pr.comments[at];
|
|
824
|
+
const editable = !pr.comments.slice(at + 1).some(x => !x.viewerDidAuthor && PE.MARKERS.EVIDENCE_RE.test(firstLine(x.body)));
|
|
825
|
+
return Object.freeze({ id: c.commentId, marker: firstLine(c.body), editable });
|
|
826
|
+
}
|
|
827
|
+
|
|
754
828
|
/**
|
|
755
829
|
* D-VERIFY-APPROVAL (D6): each login's LATEST decisive review — APPROVED,
|
|
756
830
|
* CHANGES_REQUESTED or DISMISSED — in chronological order.
|
|
@@ -1437,22 +1511,24 @@ function bodyUpdate(io, plan, ev, body) {
|
|
|
1437
1511
|
|
|
1438
1512
|
/**
|
|
1439
1513
|
* The comment (D-VERIFY-PUBLICATION) and whether it is already posted: only the
|
|
1440
|
-
* viewer's own comment whose first line is this
|
|
1441
|
-
* counts, so a spoofed marker never suppresses a post
|
|
1442
|
-
*
|
|
1514
|
+
* viewer's own NEWEST evidence comment (the edit target) whose first line is this
|
|
1515
|
+
* exact marker (head and key) counts, so a spoofed marker never suppresses a post
|
|
1516
|
+
* and an older exact match behind a newer own comment of another key reads `no`
|
|
1517
|
+
* (the newer one is what a publish edits). `off`, or nothing to record, is no
|
|
1518
|
+
* comment (`n/a`). Null when it fails to render.
|
|
1443
1519
|
*
|
|
1444
1520
|
* @param {{ tps: readonly any[] }} plan
|
|
1445
1521
|
* @param {any} ev
|
|
1446
1522
|
* @param {'full' | 'off' | 'stub'} mode
|
|
1447
|
-
* @param {
|
|
1523
|
+
* @param {EditTarget | null} target
|
|
1448
1524
|
* @returns {{ text: string, posted: 'yes' | 'no' | 'n/a' } | null}
|
|
1449
1525
|
*/
|
|
1450
|
-
function commentUpdate(plan, ev, mode,
|
|
1526
|
+
function commentUpdate(plan, ev, mode, target) {
|
|
1451
1527
|
if (mode === 'off' || (ev.records.length === 0 && ev.exceptions.length === 0)) return { text: '', posted: 'n/a' };
|
|
1452
1528
|
const rendered = PE.render(plan, ev, mode);
|
|
1453
1529
|
if (!rendered.ok) return null;
|
|
1454
1530
|
const marker = firstLine(rendered.value.text);
|
|
1455
|
-
const posted =
|
|
1531
|
+
const posted = target !== null && target.marker === marker ? 'yes' : 'no';
|
|
1456
1532
|
return { text: rendered.value.text, posted };
|
|
1457
1533
|
}
|
|
1458
1534
|
|
|
@@ -1491,25 +1567,44 @@ function writeOutputs(args, body, blockText, commentText, staleText) {
|
|
|
1491
1567
|
return writes.every(([file, text]) => file === null || writeTextFile(file, text));
|
|
1492
1568
|
}
|
|
1493
1569
|
|
|
1570
|
+
/**
|
|
1571
|
+
* @typedef {{ outcome: Outcome, target: EditTarget | null }} VerifyPass
|
|
1572
|
+
* The verify result and the viewer's edit target from the same PR snapshot (null on
|
|
1573
|
+
* every refusal). The target stays in memory: it never reaches stdout, a flag or a file.
|
|
1574
|
+
*/
|
|
1575
|
+
|
|
1494
1576
|
/**
|
|
1577
|
+
* @param {Io} io
|
|
1578
|
+
* @param {number} code
|
|
1579
|
+
* @param {string} reason a fixed phrase from this file, never input bytes
|
|
1580
|
+
* @returns {VerifyPass}
|
|
1581
|
+
*/
|
|
1582
|
+
function refusedPass(io, code, reason) {
|
|
1583
|
+
return { outcome: refuse(io, code, reason), target: null };
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* One verify pass: the Outcome the `verify` subcommand prints, and the edit target
|
|
1588
|
+
* the same snapshot gave. `posted` in the EVIDENCE line is taken from that target.
|
|
1589
|
+
*
|
|
1495
1590
|
* @param {Io} io
|
|
1496
1591
|
* @param {Extract<ParsedArgs, { kind: 'verify' }>} args
|
|
1497
1592
|
* @param {MainDeps} deps
|
|
1498
|
-
* @returns {
|
|
1593
|
+
* @returns {VerifyPass}
|
|
1499
1594
|
*/
|
|
1500
|
-
function
|
|
1595
|
+
function verifyPass(io, args, deps) {
|
|
1501
1596
|
// Inputs first: an unusable evidence file or state directory decides before any call.
|
|
1502
1597
|
/** @type {EvidenceInput | null} */
|
|
1503
1598
|
let evidence = null;
|
|
1504
1599
|
if (args.evidence !== null) {
|
|
1505
1600
|
evidence = loadEvidence(args.evidence);
|
|
1506
|
-
if (evidence === null) return
|
|
1601
|
+
if (evidence === null) return refusedPass(io, EXIT_CODES.INPUT_UNUSABLE, 'evidence file unusable');
|
|
1507
1602
|
if (evidence.malformed > 0) io.notes.add('malformed-claims');
|
|
1508
1603
|
}
|
|
1509
|
-
if (args.state !== null && !isRealDirectory(args.state)) return
|
|
1604
|
+
if (args.state !== null && !isRealDirectory(args.state)) return refusedPass(io, EXIT_CODES.INPUT_UNUSABLE, 'state directory unusable');
|
|
1510
1605
|
|
|
1511
1606
|
const pr = parsePrJson(prView(io, args.pr, PR_FIELDS), args.pr);
|
|
1512
|
-
if (pr === null) return
|
|
1607
|
+
if (pr === null) return refusedPass(io, EXIT_CODES.REMOTE_FAILURE, 'the PR could not be read');
|
|
1513
1608
|
|
|
1514
1609
|
const trust = resolveTrust(io, pr, args.approval);
|
|
1515
1610
|
const record = findTrustedRecord(pr, trust);
|
|
@@ -1520,12 +1615,12 @@ function runVerify(io, args, deps) {
|
|
|
1520
1615
|
// trusted record's.
|
|
1521
1616
|
const fromFile = evidence !== null && evidence.plan !== null;
|
|
1522
1617
|
const plan = fromFile ? /** @type {any} */ (evidence).plan : bodyPlan(pr.body);
|
|
1523
|
-
if (plan === null) return
|
|
1618
|
+
if (plan === null) return refusedPass(io, EXIT_CODES.INPUT_UNUSABLE, 'the PR body test-plan block cannot serve as the plan');
|
|
1524
1619
|
if (!fromFile && !coversRecord(plan, record)) {
|
|
1525
|
-
return
|
|
1620
|
+
return refusedPass(io, EXIT_CODES.INPUT_UNUSABLE, 'the PR body test-plan block drops a TP the trusted record carries');
|
|
1526
1621
|
}
|
|
1527
1622
|
const inputs = tpInputs(plan, evidence, record, fromFile);
|
|
1528
|
-
if (inputs === null) return
|
|
1623
|
+
if (inputs === null) return refusedPass(io, EXIT_CODES.OUTPUT_GATE_REFUSED, 'a TP line failed its grammar');
|
|
1529
1624
|
|
|
1530
1625
|
const repo = prepareRepo(io, pr);
|
|
1531
1626
|
/** @type {RunMemo} */
|
|
@@ -1539,7 +1634,7 @@ function runVerify(io, args, deps) {
|
|
|
1539
1634
|
: record.kind === 'record' ? record.exceptions : [];
|
|
1540
1635
|
const key = PE.dedupeKey({ records, exceptions }, sha256);
|
|
1541
1636
|
const tally = PE.tally(records.map(r => r.state));
|
|
1542
|
-
if (!key.ok || !tally.ok) return
|
|
1637
|
+
if (!key.ok || !tally.ok) return refusedPass(io, EXIT_CODES.OUTPUT_GATE_REFUSED, 'the records failed their grammar');
|
|
1543
1638
|
|
|
1544
1639
|
const mode = publicationMode(args.publication);
|
|
1545
1640
|
const ev = {
|
|
@@ -1550,8 +1645,9 @@ function runVerify(io, args, deps) {
|
|
|
1550
1645
|
exceptions,
|
|
1551
1646
|
};
|
|
1552
1647
|
const body = bodyUpdate(io, plan, ev, pr.body);
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1648
|
+
const target = editTarget(pr);
|
|
1649
|
+
const comment = commentUpdate(plan, ev, mode, target);
|
|
1650
|
+
if (body === null || comment === null) return refusedPass(io, EXIT_CODES.OUTPUT_GATE_REFUSED, 'the block or the comment failed to render');
|
|
1555
1651
|
|
|
1556
1652
|
const line = gateEvidenceLine(deps, {
|
|
1557
1653
|
pr: pr.number,
|
|
@@ -1565,12 +1661,22 @@ function runVerify(io, args, deps) {
|
|
|
1565
1661
|
posted: comment.posted,
|
|
1566
1662
|
body: body.changed ? 'changed' : 'same',
|
|
1567
1663
|
}, plan.tps.length);
|
|
1568
|
-
if (line === null) return
|
|
1664
|
+
if (line === null) return refusedPass(io, EXIT_CODES.OUTPUT_GATE_REFUSED, 'the EVIDENCE line failed its gate');
|
|
1569
1665
|
|
|
1570
1666
|
if (!writeOutputs(args, pr.body, body.blockText, comment.text, staleOutText(inputs, records))) {
|
|
1571
|
-
return
|
|
1667
|
+
return refusedPass(io, EXIT_CODES.WRITE_FAILED, 'an output file could not be written');
|
|
1572
1668
|
}
|
|
1573
|
-
return { code: EXIT_CODES.OK, stdout: line + '\n' };
|
|
1669
|
+
return { outcome: { code: EXIT_CODES.OK, stdout: line + '\n' }, target };
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* @param {Io} io
|
|
1674
|
+
* @param {Extract<ParsedArgs, { kind: 'verify' }>} args
|
|
1675
|
+
* @param {MainDeps} deps
|
|
1676
|
+
* @returns {Outcome}
|
|
1677
|
+
*/
|
|
1678
|
+
function runVerify(io, args, deps) {
|
|
1679
|
+
return verifyPass(io, args, deps).outcome;
|
|
1574
1680
|
}
|
|
1575
1681
|
|
|
1576
1682
|
/**
|
|
@@ -1755,7 +1861,7 @@ const EVIDENCE_UNAVAILABLE_LINE = 'TRACEABILITY: DEGRADED (evidence unavailable)
|
|
|
1755
1861
|
*/
|
|
1756
1862
|
|
|
1757
1863
|
const BODY_STATUSES = Object.freeze(['EDITED', 'UNCHANGED', 'SKIPPED']);
|
|
1758
|
-
const COMMENT_STATUSES = Object.freeze(['POSTED', 'SKIPPED', 'OFF']);
|
|
1864
|
+
const COMMENT_STATUSES = Object.freeze(['POSTED', 'SKIPPED', 'OFF', 'EDITED']);
|
|
1759
1865
|
|
|
1760
1866
|
/**
|
|
1761
1867
|
* @param {string} status
|
|
@@ -1900,6 +2006,18 @@ function ghWrite(io, args) {
|
|
|
1900
2006
|
return { ok: r.ok, serverError: isServerError(r) };
|
|
1901
2007
|
}
|
|
1902
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* The DEGRADED part for a gh write that failed: the 5xx phrase after a server error,
|
|
2011
|
+
* else the phrase of the failed step. It runs no gh call.
|
|
2012
|
+
*
|
|
2013
|
+
* @param {{ ok: boolean, serverError: boolean }} result what ghWrite returned
|
|
2014
|
+
* @param {string} reason a fixed phrase from this file
|
|
2015
|
+
* @returns {Part}
|
|
2016
|
+
*/
|
|
2017
|
+
function writeFailedPart(result, reason) {
|
|
2018
|
+
return degradedPart(result.serverError ? '5xx on update-pr-evidence' : reason);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1903
2021
|
/**
|
|
1904
2022
|
* The context for the writes: a gh budget and a deadline of their own, the same
|
|
1905
2023
|
* notes. The throttle flag carries over, so a rate limit that verify hit stops the
|
|
@@ -1938,38 +2056,77 @@ function publishBody(io, pr, fields, files) {
|
|
|
1938
2056
|
if (outcome === 'conflict') return okPart('SKIPPED');
|
|
1939
2057
|
if (outcome !== 'ok' && outcome !== 'resplice') return degradedPart('splice ' + outcome);
|
|
1940
2058
|
const edit = ghWrite(io, ['pr', 'edit', String(pr), '--body-file', files.body]);
|
|
1941
|
-
if (!edit.ok) return
|
|
2059
|
+
if (!edit.ok) return writeFailedPart(edit, 'gh pr edit failed');
|
|
1942
2060
|
const back = runReadback(io, { kind: 'readback', pr, expect: files.body });
|
|
1943
2061
|
return back.code === EXIT_CODES.OK && back.stdout === 'READBACK ok\n' ? okPart('EDITED') : degradedPart('body read-back mismatch');
|
|
1944
2062
|
}
|
|
1945
2063
|
|
|
1946
2064
|
/**
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
2065
|
+
* Whether the comment just edited now reads back as the scrubbed text: one GET of it
|
|
2066
|
+
* as a JSON string, compared after one trailing newline is normalised on each side.
|
|
2067
|
+
* The edit itself runs silent, so this is the only read of the comment. Any failure
|
|
2068
|
+
* to read or parse is "no". Neither the comment nor gh's stderr is printed.
|
|
2069
|
+
*
|
|
2070
|
+
* @param {Io} io the write-stage context
|
|
2071
|
+
* @param {string} id a comment id commentIdOf produced
|
|
2072
|
+
* @param {string} expected the scrubbed comment text (never the unscrubbed file)
|
|
2073
|
+
* @returns {boolean}
|
|
2074
|
+
*/
|
|
2075
|
+
function commentReadsBack(io, id, expected) {
|
|
2076
|
+
const r = gh(io, ['api', 'repos/{owner}/{repo}/issues/comments/' + commentIdArg(id), '--jq', '.body | @json'], COMMENT_READ_MAX_BUFFER);
|
|
2077
|
+
if (!r.ok) return false;
|
|
2078
|
+
try {
|
|
2079
|
+
const body = JSON.parse(stripOneNewline(r.stdout.toString('utf8')));
|
|
2080
|
+
return typeof body === 'string' && stripOneNewline(body) === stripOneNewline(expected);
|
|
2081
|
+
} catch (_) {
|
|
2082
|
+
return false;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/**
|
|
2087
|
+
* The comment half: the evidence comment, scrubbed (D11) and re-measured after the
|
|
2088
|
+
* scrub. When the viewer's own evidence comment is a safe target (no strict marker
|
|
2089
|
+
* from another login follows it), that comment is edited in place (one PATCH of the
|
|
2090
|
+
* scrubbed file) and read back against it. Otherwise a new comment is posted once.
|
|
2091
|
+
* A comment that already carries this head and key is skipped. An own target with no
|
|
2092
|
+
* readable id is DEGRADED before the scrub, with no write. A failed edit or
|
|
2093
|
+
* read-back is a DEGRADED result and never falls back to a post. Nothing is deleted.
|
|
1949
2094
|
*
|
|
1950
2095
|
* @param {Io} io the write-stage context
|
|
1951
2096
|
* @param {number} pr
|
|
1952
2097
|
* @param {{ posted: string }} fields the EVIDENCE line's fields
|
|
1953
2098
|
* @param {{ commentRaw: string, comment: string }} files
|
|
2099
|
+
* @param {EditTarget | null} target the viewer's newest evidence comment in the snapshot
|
|
2100
|
+
* verify read, or null when there is none
|
|
1954
2101
|
* @returns {Part}
|
|
1955
2102
|
*/
|
|
1956
|
-
function publishComment(io, pr, fields, files) {
|
|
2103
|
+
function publishComment(io, pr, fields, files, target) {
|
|
1957
2104
|
if (fields.posted === 'yes') return okPart('SKIPPED');
|
|
1958
2105
|
if (fields.posted === 'n/a') return okPart('OFF');
|
|
2106
|
+
const edit = target !== null && target.editable;
|
|
2107
|
+
if (edit && target.id === '') return degradedPart('comment id unreadable');
|
|
1959
2108
|
if (!scrub(io, files.commentRaw, files.comment)) return degradedPart('redaction unavailable');
|
|
1960
2109
|
const text = readTextFile(files.comment);
|
|
1961
2110
|
if (text === null || text === '') return degradedPart('redaction unavailable');
|
|
1962
2111
|
if (text.length > COMMENT_POST_MAX_CHARS) return degradedPart('comment over size cap');
|
|
2112
|
+
if (edit) {
|
|
2113
|
+
const patch = ghWrite(io, ['api', '--method', 'PATCH',
|
|
2114
|
+
'repos/{owner}/{repo}/issues/comments/' + commentIdArg(target.id),
|
|
2115
|
+
'-F', 'body=@' + files.comment, '--silent']);
|
|
2116
|
+
if (!patch.ok) return writeFailedPart(patch, 'comment edit failed');
|
|
2117
|
+
return commentReadsBack(io, target.id, text) ? okPart('EDITED') : degradedPart('comment read-back mismatch');
|
|
2118
|
+
}
|
|
1963
2119
|
const post = ghWrite(io, ['pr', 'comment', String(pr), '--body-file', files.comment]);
|
|
1964
|
-
|
|
1965
|
-
return degradedPart(post.serverError ? '5xx on update-pr-evidence' : 'gh pr comment failed');
|
|
2120
|
+
return post.ok ? okPart('POSTED') : writeFailedPart(post, 'gh pr comment failed');
|
|
1966
2121
|
}
|
|
1967
2122
|
|
|
1968
2123
|
/**
|
|
1969
2124
|
* The whole update-pr-evidence operation, in the order the operation always had:
|
|
1970
2125
|
* verify; only on `body:changed` scrub, splice, `gh pr edit`, read back; only on
|
|
1971
|
-
* `posted:no` scrub
|
|
1972
|
-
*
|
|
2126
|
+
* `posted:no` scrub, then edit the viewer's own evidence comment in place and read it
|
|
2127
|
+
* back, or post a new one when there is none to edit or a newer evidence comment from
|
|
2128
|
+
* another login follows it. The work files live in one private directory this call
|
|
2129
|
+
* makes and removes.
|
|
1973
2130
|
*
|
|
1974
2131
|
* @param {Io} io
|
|
1975
2132
|
* @param {Extract<ParsedArgs, { kind: 'publish' }>} args
|
|
@@ -2000,18 +2157,18 @@ function runPublish(io, args, deps) {
|
|
|
2000
2157
|
commentRaw: file('comment.raw'),
|
|
2001
2158
|
comment: file('comment'),
|
|
2002
2159
|
};
|
|
2003
|
-
const verified =
|
|
2160
|
+
const verified = verifyPass(io, {
|
|
2004
2161
|
kind: 'verify', pr: args.pr, publication: args.publication, evidence: args.evidence, state,
|
|
2005
2162
|
blockOut: files.notesRaw, commentOut: files.commentRaw, staleOut: null, approval: false,
|
|
2006
2163
|
}, deps);
|
|
2007
|
-
if (verified.code !== EXIT_CODES.OK) return evidenceUnavailable(io, 'verify did not answer');
|
|
2008
|
-
const line = stripOneNewline(verified.stdout);
|
|
2164
|
+
if (verified.outcome.code !== EXIT_CODES.OK) return evidenceUnavailable(io, 'verify did not answer');
|
|
2165
|
+
const line = stripOneNewline(verified.outcome.stdout);
|
|
2009
2166
|
const parsed = PE.parseEvidenceLine(line);
|
|
2010
2167
|
if (!parsed.ok || parsed.value.pr !== args.pr) return evidenceUnavailable(io, 'verify answered something else');
|
|
2011
2168
|
|
|
2012
2169
|
const stage = writeStageIo(io);
|
|
2013
2170
|
const body = publishBody(stage, args.pr, parsed.value, files);
|
|
2014
|
-
const comment = publishComment(stage, args.pr, parsed.value, files);
|
|
2171
|
+
const comment = publishComment(stage, args.pr, parsed.value, files, verified.target);
|
|
2015
2172
|
return { code: EXIT_CODES.OK, stdout: renderPublishBlock(line, body, comment) };
|
|
2016
2173
|
} finally {
|
|
2017
2174
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) { /* the OS clears its temp directory */ }
|
|
@@ -2162,21 +2319,28 @@ function parseJsonOrNull(text) {
|
|
|
2162
2319
|
}
|
|
2163
2320
|
|
|
2164
2321
|
/**
|
|
2165
|
-
* The PR's review decision
|
|
2166
|
-
*
|
|
2167
|
-
* ladder never lets that reach READY.
|
|
2322
|
+
* The PR's review decision, merge state and head, from one `gh pr view` read. The
|
|
2323
|
+
* decision answers `""` or null when no rule asks for a review; a string outside the
|
|
2324
|
+
* three documented values reads UNRECOGNISED, and the ladder never lets that reach READY.
|
|
2325
|
+
* `mergeable` is the string GitHub printed, or null when the key is absent or not a
|
|
2326
|
+
* string; `head` is the PR head when it is 40 lowercase hex, else null. Neither makes the
|
|
2327
|
+
* decision unreadable: the CI read treats them as unknown and never as a pass.
|
|
2168
2328
|
*
|
|
2169
2329
|
* @param {Io} io
|
|
2170
2330
|
* @param {number} pr
|
|
2171
|
-
* @returns {Read<string | null>}
|
|
2331
|
+
* @returns {Read<{ decision: string | null, mergeable: string | null, head: string | null }>}
|
|
2172
2332
|
*/
|
|
2173
|
-
function
|
|
2174
|
-
const r = gh(io, ['pr', 'view', String(pr), '--json', 'reviewDecision'], LINE_MAX_BUFFER);
|
|
2333
|
+
function readPrView(io, pr) {
|
|
2334
|
+
const r = gh(io, ['pr', 'view', String(pr), '--json', 'reviewDecision,mergeable,headRefOid'], LINE_MAX_BUFFER);
|
|
2175
2335
|
if (!r.ok) return { ok: false, reason: readFailure(r) };
|
|
2176
|
-
const
|
|
2177
|
-
|
|
2336
|
+
const fields = obj(parseJsonOrNull(r.stdout.toString('utf8')));
|
|
2337
|
+
const value = fields.reviewDecision;
|
|
2338
|
+
const mergeable = typeof fields.mergeable === 'string' ? fields.mergeable : null;
|
|
2339
|
+
const head = typeof fields.headRefOid === 'string' && SHA40_RE.test(fields.headRefOid) ? fields.headRefOid : null;
|
|
2340
|
+
if (value === null || value === '') return { ok: true, value: { decision: null, mergeable, head } };
|
|
2178
2341
|
if (typeof value !== 'string') return { ok: false, reason: 'review decision unreadable' };
|
|
2179
|
-
|
|
2342
|
+
const decision = ['APPROVED', 'CHANGES_REQUESTED', 'REVIEW_REQUIRED'].includes(value) ? value : 'UNRECOGNISED';
|
|
2343
|
+
return { ok: true, value: { decision, mergeable, head } };
|
|
2180
2344
|
}
|
|
2181
2345
|
|
|
2182
2346
|
/**
|
|
@@ -2215,37 +2379,91 @@ function readThreads(io, pr) {
|
|
|
2215
2379
|
return { ok: true, value: { unresolved, approximate: total > THREADS_PAGE } };
|
|
2216
2380
|
}
|
|
2217
2381
|
|
|
2382
|
+
/** CI that could not be established: the ladder reads it as `status unknown`, never READY. */
|
|
2383
|
+
const CI_INDETERMINATE = Object.freeze({ status: 'INDETERMINATE', failing: '-' });
|
|
2384
|
+
|
|
2385
|
+
/**
|
|
2386
|
+
* D-READINESS-WORKFLOWS: whether the repository has an active workflow file, by ci-wait's
|
|
2387
|
+
* one read-only call and parser. A call that fails or is refused, and a reply the parser
|
|
2388
|
+
* cannot read, give null: not knowing, never "no workflow file".
|
|
2389
|
+
*
|
|
2390
|
+
* @param {Io} io
|
|
2391
|
+
* @returns {boolean | null}
|
|
2392
|
+
*/
|
|
2393
|
+
function probeWorkflows(io) {
|
|
2394
|
+
const r = gh(io, CW.WORKFLOWS_ARGS, READINESS_MAX_BUFFER);
|
|
2395
|
+
return r.ok ? CW.parseActiveWorkflows(r.stdout.toString('utf8')) : null;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
/**
|
|
2399
|
+
* An empty check list: ci-wait's merge gate decides (D-CI-WORKFLOWS). The workflow list
|
|
2400
|
+
* is asked only when the gate asks for it, so a PR that cannot be NO_CI costs no call. A
|
|
2401
|
+
* gate answer other than keep is INDETERMINATE.
|
|
2402
|
+
*
|
|
2403
|
+
* @param {Io} io
|
|
2404
|
+
* @param {string | null} mergeable
|
|
2405
|
+
* @returns {{ status: string, failing: string }}
|
|
2406
|
+
*/
|
|
2407
|
+
function readEmptyCi(io, mergeable) {
|
|
2408
|
+
const first = CW.mergeGate('NO_CI', mergeable);
|
|
2409
|
+
const gate = first === 'probe' ? CW.mergeGate('NO_CI', mergeable, probeWorkflows(io)) : first;
|
|
2410
|
+
return gate === 'keep' ? { status: 'NO_CI', failing: '-' } : CI_INDETERMINATE;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2218
2413
|
/**
|
|
2219
|
-
* D-READINESS-CLASSIFIER: the CI status,
|
|
2220
|
-
*
|
|
2221
|
-
*
|
|
2222
|
-
* ci-wait's
|
|
2414
|
+
* D-READINESS-CLASSIFIER: the CI status, read once. The checks come from `gh pr checks`
|
|
2415
|
+
* over name,state,bucket,workflow, with check-ci-status's exit rule (exit 0 or 8 is a
|
|
2416
|
+
* result; any other failure is INDETERMINATE). The rows come from ci-wait's parser, and the
|
|
2417
|
+
* status from ci-wait's classifier (D-CI-WORKFLOW-RULE) and merge gate (D-CI-MERGE-STATE,
|
|
2418
|
+
* D-CI-WORKFLOWS), where the rules are written, so the two scripts cannot disagree. A
|
|
2419
|
+
* conflicting PR reads INDETERMINATE before any checks call, and "no checks reported" or
|
|
2420
|
+
* `[]` goes to readEmptyCi. An unknown merge state, a failed workflow-list call and an
|
|
2421
|
+
* unreadable reply are INDETERMINATE, never DEGRADED. Failing names are sanitized and
|
|
2422
|
+
* bounded by ci-wait's own formatter. PASSING does not prove that the test workflow ran by
|
|
2423
|
+
* name.
|
|
2223
2424
|
*
|
|
2224
2425
|
* @param {Io} io
|
|
2225
2426
|
* @param {number} pr
|
|
2427
|
+
* @param {string | null} mergeable GitHub's merge state of the PR, from readPrView
|
|
2226
2428
|
* @returns {{ status: string, failing: string }}
|
|
2227
2429
|
*/
|
|
2228
|
-
function readCi(io, pr) {
|
|
2229
|
-
|
|
2230
|
-
const
|
|
2231
|
-
if (!answered(r)) return
|
|
2430
|
+
function readCi(io, pr, mergeable) {
|
|
2431
|
+
if (CW.isConflicting(mergeable)) return CI_INDETERMINATE;
|
|
2432
|
+
const r = gh(io, ['pr', 'checks', String(pr), '--json', 'name,state,bucket,workflow'], READINESS_MAX_BUFFER);
|
|
2433
|
+
if (!answered(r)) return CI_INDETERMINATE;
|
|
2232
2434
|
if (r.status !== 0 && r.status !== 8) {
|
|
2233
2435
|
if (/Could not resolve to a PullRequest/.test(r.stderr)) return { status: 'NO_PR', failing: '-' };
|
|
2234
|
-
return /no checks reported/i.test(r.stderr) ?
|
|
2235
|
-
}
|
|
2236
|
-
const rows = parseJsonOrNull(r.stdout.toString('utf8'));
|
|
2237
|
-
if (!Array.isArray(rows) || rows.length > READINESS_MAX_CHECKS) return indeterminate;
|
|
2238
|
-
if (rows.length === 0) return { status: 'NO_CI', failing: '-' };
|
|
2239
|
-
const checks = [];
|
|
2240
|
-
for (const row of rows) {
|
|
2241
|
-
const x = obj(row);
|
|
2242
|
-
if (typeof x.name !== 'string' || typeof x.bucket !== 'string') return indeterminate;
|
|
2243
|
-
checks.push({ name: x.name, bucket: x.bucket });
|
|
2436
|
+
return /no checks reported/i.test(r.stderr) ? readEmptyCi(io, mergeable) : CI_INDETERMINATE;
|
|
2244
2437
|
}
|
|
2438
|
+
const checks = CW.parseChecks(r.stdout.toString('utf8'));
|
|
2439
|
+
if (checks === null) return CI_INDETERMINATE;
|
|
2440
|
+
if (checks.length === 0) return readEmptyCi(io, mergeable);
|
|
2245
2441
|
const c = CW.classifyChecks(checks);
|
|
2442
|
+
if (CW.mergeGate(c.status, mergeable, null) !== 'keep') return CI_INDETERMINATE;
|
|
2246
2443
|
return { status: c.status, failing: CW.formatNames(c.failing) };
|
|
2247
2444
|
}
|
|
2248
2445
|
|
|
2446
|
+
/**
|
|
2447
|
+
* D-READINESS-HEAD: CI counts for one head. `gh pr checks` answers for whatever head the
|
|
2448
|
+
* PR has when it is called, and the evidence is read after it, so a push in between would
|
|
2449
|
+
* pair a CI result with evidence of another head. PASSING and NO_CI stand only when the
|
|
2450
|
+
* head read with the review decision, before the checks call, and the head the evidence
|
|
2451
|
+
* was read at are both 40 lowercase hex and equal; otherwise CI is INDETERMINATE. FAILING,
|
|
2452
|
+
* PENDING and the rest stay: they hold the ladder back on their own.
|
|
2453
|
+
*
|
|
2454
|
+
* @param {{ status: string, failing: string }} ci
|
|
2455
|
+
* @param {unknown} viewHead the head read before the checks call, or null
|
|
2456
|
+
* @param {unknown} evidenceHead the head the evidence was read at, or null when it is unknown
|
|
2457
|
+
* @returns {{ status: string, failing: string }}
|
|
2458
|
+
*/
|
|
2459
|
+
function bindCiHead(ci, viewHead, evidenceHead) {
|
|
2460
|
+
if (ci.status !== 'PASSING' && ci.status !== 'NO_CI') return ci;
|
|
2461
|
+
/** @param {unknown} head */
|
|
2462
|
+
const isHead = head => typeof head === 'string' && SHA40_RE.test(head);
|
|
2463
|
+
const bound = isHead(viewHead) && isHead(evidenceHead) && viewHead === evidenceHead;
|
|
2464
|
+
return bound ? ci : CI_INDETERMINATE;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2249
2467
|
/**
|
|
2250
2468
|
* The test-plan evidence at the head, by `verify --approval`: known only on exit 0
|
|
2251
2469
|
* with one EVIDENCE line for this PR (the old step 4's rule).
|
|
@@ -2275,6 +2493,15 @@ function approvalText(require, evidence) {
|
|
|
2275
2493
|
return evidence !== null && evidence.approval === 'yes' ? 'yes' : 'no';
|
|
2276
2494
|
}
|
|
2277
2495
|
|
|
2496
|
+
/**
|
|
2497
|
+
* @param {string | null} decision the review decision from readPrView
|
|
2498
|
+
* @returns {string} the word the Details line prints for it
|
|
2499
|
+
*/
|
|
2500
|
+
function decisionLabel(decision) {
|
|
2501
|
+
if (decision === null) return 'none';
|
|
2502
|
+
return decision === 'UNRECOGNISED' ? 'unrecognised' : decision;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2278
2505
|
/**
|
|
2279
2506
|
* A readiness answer that could not be reached: a DEGRADED block (exit 2) whose Details
|
|
2280
2507
|
* say `unavailable` for what was never read.
|
|
@@ -2308,15 +2535,17 @@ function readinessDegraded(pr, reason, known) {
|
|
|
2308
2535
|
*/
|
|
2309
2536
|
function runReadiness(io, args, deps) {
|
|
2310
2537
|
const unreadApproval = approvalText(args.requireNonAuthor, null);
|
|
2311
|
-
const
|
|
2312
|
-
if (!
|
|
2313
|
-
const
|
|
2538
|
+
const view = readPrView(io, args.pr);
|
|
2539
|
+
if (!view.ok) return readinessDegraded(args.pr, view.reason, { approval: unreadApproval });
|
|
2540
|
+
const reviewDecision = view.value.decision;
|
|
2541
|
+
const decisionText = decisionLabel(reviewDecision);
|
|
2314
2542
|
const threads = readThreads(io, args.pr);
|
|
2315
2543
|
if (!threads.ok) {
|
|
2316
2544
|
return readinessDegraded(args.pr, threads.reason, { decision: decisionText, approval: unreadApproval });
|
|
2317
2545
|
}
|
|
2318
|
-
const
|
|
2546
|
+
const ciRead = readCi(io, args.pr, view.value.mergeable);
|
|
2319
2547
|
const fields = readEvidence(io, args.pr, deps);
|
|
2548
|
+
const ci = bindCiHead(ciRead, view.value.head, fields === null ? null : fields.head);
|
|
2320
2549
|
/** @type {EvidenceFacts | null} */
|
|
2321
2550
|
const evidence = fields === null ? null : {
|
|
2322
2551
|
total: fields.total,
|
|
@@ -2328,7 +2557,7 @@ function runReadiness(io, args, deps) {
|
|
|
2328
2557
|
const verdict = readinessVerdict({
|
|
2329
2558
|
unresolved: threads.value.unresolved,
|
|
2330
2559
|
approximate: threads.value.approximate,
|
|
2331
|
-
decision:
|
|
2560
|
+
decision: reviewDecision,
|
|
2332
2561
|
ci: ci.status,
|
|
2333
2562
|
failing: ci.failing,
|
|
2334
2563
|
evidence,
|
|
@@ -2532,8 +2761,11 @@ module.exports = Object.freeze({
|
|
|
2532
2761
|
BODY_STATUSES,
|
|
2533
2762
|
COMMENT_STATUSES,
|
|
2534
2763
|
readinessVerdict,
|
|
2764
|
+
bindCiHead,
|
|
2535
2765
|
renderPublishBlock,
|
|
2536
2766
|
parsePublishBlock,
|
|
2537
2767
|
renderReadinessBlock,
|
|
2538
2768
|
parseReadinessBlock,
|
|
2769
|
+
commentIdOf,
|
|
2770
|
+
editTarget,
|
|
2539
2771
|
});
|