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
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// The CI wait for one pushed head (Token Diet wave 2, #421). /implement Phase 9 and
|
|
5
5
|
// /resolve Phase 8 call it inline, through the Bash tool, instead of re-spawning the
|
|
6
6
|
// Git agent once a minute: a poll costs no model tokens, so the wait happens here.
|
|
7
|
-
// It reads CI with `gh pr view` and `gh pr checks
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// It reads CI with `gh pr view` and `gh pr checks`, plus a read-only `gh api` probe of
|
|
8
|
+
// the repository's workflow list when the check list stays empty (D-CI-WORKFLOWS): at
|
|
9
|
+
// most one answered probe per run; a failed probe is a failed poll, and the next poll
|
|
10
|
+
// probes again. It classifies the head it was told about, and prints exactly one line.
|
|
11
|
+
// Installed beside hud.sh and redact-secrets.cjs under ~/.devflow/scripts/.
|
|
10
12
|
//
|
|
11
13
|
// Usage:
|
|
12
14
|
// node ci-wait.cjs --pr <n> --head <sha>
|
|
@@ -24,12 +26,19 @@
|
|
|
24
26
|
// writes it. Exit 0 whenever a line was printed; a reader treats a missing or
|
|
25
27
|
// unparseable line, an unlisted status or a non-zero exit as INDETERMINATE.
|
|
26
28
|
//
|
|
27
|
-
// Design constraints (binding)
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
29
|
+
// Design constraints (binding). The text of each verdict rule is written once, at the
|
|
30
|
+
// marker named below; this list points to it.
|
|
31
|
+
// - D-CI-WAIT-INLINE: this is plumbing. The only judgment in it is the classifier
|
|
32
|
+
// (D-CI-CLASSIFIER, D-CI-WORKFLOW-RULE) and the merge gate (D-CI-MERGE-STATE,
|
|
33
|
+
// D-CI-WORKFLOWS, D-CI-PR-STATE). Every subprocess is `gh`, spawned through exec()
|
|
34
|
+
// with an argv array and `shell: false`, stdin ignored, a timeout and a maxBuffer.
|
|
35
|
+
// gh output never reaches stdout raw: a check name passes sanitizeName() first.
|
|
36
|
+
// - D-CI-WORKFLOW-RULE: see classifyChecks. PASSING does not prove that the test
|
|
37
|
+
// workflow ran by name.
|
|
38
|
+
// - D-CI-MERGE-STATE, D-CI-WORKFLOWS: see mergeGate. The workflow files are listed,
|
|
39
|
+
// never opened: no workflow YAML is parsed, so which events a workflow reacts to is
|
|
40
|
+
// not judged.
|
|
41
|
+
// - D-CI-PR-STATE, D-CI-HEAD-BINDING: see waitForCi.
|
|
33
42
|
// - D-CI-NO-LITERAL-SLEEP: the wait happens inside node, through clock.sleep(),
|
|
34
43
|
// never through a shell `sleep`. Claude Code 2.1.294 blocks any Bash command
|
|
35
44
|
// that starts with a literal `sleep N`, so no prompt and no script may wait that
|
|
@@ -42,14 +51,31 @@
|
|
|
42
51
|
// name lists), and every wait is capped at CI_WAIT_MAX_SECONDS.
|
|
43
52
|
//
|
|
44
53
|
// gh facts this file rests on, probed on gh 2.88.1 on 2026-10-09:
|
|
45
|
-
// - `gh pr view 999999999 --json headRefOid` exits 1 with stderr
|
|
46
|
-
// not resolve to a PullRequest with the number of 999999999.
|
|
47
|
-
// (repository.pullRequest)` and an empty stdout
|
|
48
|
-
//
|
|
54
|
+
// - `gh pr view 999999999 --json headRefOid,mergeable,state` exits 1 with stderr
|
|
55
|
+
// `GraphQL: Could not resolve to a PullRequest with the number of 999999999.
|
|
56
|
+
// (repository.pullRequest)` and an empty stdout (re-probed with `mergeable` and
|
|
57
|
+
// `state` in the field list on 2026-10-22: the same text). Only that text on exit 1
|
|
58
|
+
// is NO_PR; any other non-zero exit is a gh failure and never PASSING.
|
|
49
59
|
// - `gh pr checks <n> --json` offers exactly bucket, completedAt, description,
|
|
50
60
|
// event, link, name, startedAt, state and workflow. There is no SHA field, and it
|
|
51
61
|
// reports only the PR's current head. So the head is bound by comparing
|
|
52
62
|
// `gh pr view --json headRefOid` with `--head` (D-CI-HEAD-BINDING).
|
|
63
|
+
// - `workflow` names the GitHub Actions workflow of a check, and is "" for a commit
|
|
64
|
+
// status and for a third-party app's check run (probed on public PRs, gh 2.88.1,
|
|
65
|
+
// 2026-10-22).
|
|
66
|
+
// - `gh pr view --json mergeable` is MERGEABLE, CONFLICTING or UNKNOWN. GitHub
|
|
67
|
+
// computes it lazily, so UNKNOWN is common right after a push or a base move. A
|
|
68
|
+
// merged PR reads `state` MERGED with `mergeable` UNKNOWN (probed on gh 2.88.1,
|
|
69
|
+
// 2026-10-22), which is why the poll asks `state` (D-CI-PR-STATE).
|
|
70
|
+
// - `gh api` fills the placeholders `{owner}` and `{repo}` from the repository of the
|
|
71
|
+
// current directory (`gh api --help`), and a call with no field flag is a GET.
|
|
72
|
+
// `repos/{owner}/{repo}/actions/workflows?per_page=100` answers `{ total_count,
|
|
73
|
+
// workflows: [{ path, state, ... }] }`, one row per workflow, with `state` active
|
|
74
|
+
// for a live one and a `path` such as `.github/workflows/ci.yml` (probed on gh
|
|
75
|
+
// 2.88.1, 2026-10-22). It is used rather than `gh workflow list` because
|
|
76
|
+
// `total_count` tells a whole list from a cut one. A row whose path is not under
|
|
77
|
+
// `.github/workflows/`, such as a `dynamic/...` path that GitHub gives its own
|
|
78
|
+
// apps, is not a workflow file of the repository.
|
|
53
79
|
|
|
54
80
|
'use strict';
|
|
55
81
|
|
|
@@ -69,10 +95,13 @@ const STATUSES = Object.freeze(['PASSING', 'FAILING', 'PENDING', 'NO_CI', 'NO_PR
|
|
|
69
95
|
/**
|
|
70
96
|
* D-CI-REASONS: why an INDETERMINATE was returned. `unrecognised-bucket` also covers
|
|
71
97
|
* a set of buckets that gives no verdict (every check `skipping`): the classifier
|
|
72
|
-
* could call neither a pass nor a failure.
|
|
98
|
+
* could call neither a pass nor a failure. The other rule reasons are defined at their
|
|
99
|
+
* markers: `no-workflow-check` (D-CI-WORKFLOW-RULE, D-CI-WORKFLOWS), `merge-conflict`
|
|
100
|
+
* (D-CI-MERGE-STATE) and `not-open` (D-CI-PR-STATE).
|
|
73
101
|
*/
|
|
74
102
|
const REASONS = Object.freeze([
|
|
75
103
|
'gh-failed', 'unparseable', 'unrecognised-bucket', 'head-mismatch', 'deadline', 'usage',
|
|
104
|
+
'no-workflow-check', 'merge-conflict', 'not-open',
|
|
76
105
|
]);
|
|
77
106
|
|
|
78
107
|
/** `gh pr checks` categorises `state` into these buckets (gh 2.88.1, `--help`). */
|
|
@@ -85,10 +114,12 @@ const BUCKETS = Object.freeze(['pass', 'fail', 'pending', 'skipping', 'cancel'])
|
|
|
85
114
|
* Bash timeout of 600000 ms the callers pass
|
|
86
115
|
* POLL_INTERVAL_SECONDS the sleep between polls; polls cost no model tokens,
|
|
87
116
|
* so the cadence is set by gh latency and rate limits
|
|
88
|
-
* SETTLE_WINDOW_SECONDS how long a head mismatch
|
|
89
|
-
*
|
|
90
|
-
* checks and moves the PR
|
|
91
|
-
* push)
|
|
117
|
+
* SETTLE_WINDOW_SECONDS how long a head mismatch, an empty check list or a
|
|
118
|
+
* list of external checks only may persist before it
|
|
119
|
+
* is a verdict (GitHub creates checks and moves the PR
|
|
120
|
+
* head a few seconds after a push); the workflow probe
|
|
121
|
+
* is no window of its own
|
|
122
|
+
* WORKFLOWS_PAGE_ROWS the rows asked of the workflow list in its one page
|
|
92
123
|
* GH_CALL_CAP_SECONDS one gh call's timeout is min(this, time remaining),
|
|
93
124
|
* so the last call ends before the deadline
|
|
94
125
|
* MAX_POLLS the loop bound when the clock does not move
|
|
@@ -100,6 +131,7 @@ const SETTLE_WINDOW_SECONDS = 90;
|
|
|
100
131
|
const GH_CALL_CAP_SECONDS = 30;
|
|
101
132
|
const MAX_POLLS = 40;
|
|
102
133
|
const MAX_CONSECUTIVE_GH_FAILURES = 3;
|
|
134
|
+
const WORKFLOWS_PAGE_ROWS = 100;
|
|
103
135
|
|
|
104
136
|
/** A PR with more check rows than this is read as an unparseable reply. */
|
|
105
137
|
const MAX_CHECKS = 1000;
|
|
@@ -143,6 +175,18 @@ const LINE_RE = new RegExp(
|
|
|
143
175
|
|
|
144
176
|
const USAGE_LINE = 'CI INDETERMINATE pr=- head=- waited=0 reason=usage';
|
|
145
177
|
|
|
178
|
+
/**
|
|
179
|
+
* D-CI-WORKFLOWS: the probe, a GET of the repository's workflow list. It is a fixed
|
|
180
|
+
* argv with no field flag, no method flag and no input, and it names neither the PR nor
|
|
181
|
+
* the head; gh fills `{owner}/{repo}` from the working directory.
|
|
182
|
+
*/
|
|
183
|
+
const WORKFLOWS_ARGS = Object.freeze(['api', 'repos/{owner}/{repo}/actions/workflows?per_page=' + WORKFLOWS_PAGE_ROWS]);
|
|
184
|
+
/** A workflow file lives here; the `dynamic/...` paths GitHub adds for its own apps do not. */
|
|
185
|
+
const WORKFLOWS_DIR = '.github/workflows/';
|
|
186
|
+
|
|
187
|
+
/** The fields every `gh pr view` call of a poll asks for. */
|
|
188
|
+
const VIEW_FIELDS = 'headRefOid,mergeable,state';
|
|
189
|
+
|
|
146
190
|
// ---------------------------------------------------------------------------
|
|
147
191
|
// Types
|
|
148
192
|
// ---------------------------------------------------------------------------
|
|
@@ -156,12 +200,15 @@ const USAGE_LINE = 'CI INDETERMINATE pr=- head=- waited=0 reason=usage';
|
|
|
156
200
|
* @typedef {{ now: () => number, sleep: (ms: number) => Promise<void> }} Clock
|
|
157
201
|
* `now` is in milliseconds. Tests inject a clock whose sleep and exec advance it.
|
|
158
202
|
*
|
|
159
|
-
* @typedef {{ name: string, bucket: string }} Check
|
|
203
|
+
* @typedef {{ name: string, bucket: string, workflow?: string }} Check
|
|
204
|
+
* `workflow` is the GitHub Actions workflow name; "" or absent marks an external check.
|
|
160
205
|
* @typedef {{ status: string, reason: string | null, failing: string[], pending: string[] }} Classification
|
|
161
206
|
* @typedef {{ status: string, pr: string, head: string, failing: string[], pending: string[],
|
|
162
207
|
* waited: number, reason: string | null }} CiResult
|
|
163
208
|
* `head` is the first 7 hex characters.
|
|
164
209
|
* @typedef {{ pr: string, head: string, exec: ExecFn, clock: Clock, env?: NodeJS.ProcessEnv }} WaitInput
|
|
210
|
+
* @typedef {{ state: 'deadline' } | { state: 'failed' } | { state: 'pending', pending: string[] } | { state: 'open' }
|
|
211
|
+
* | { state: 'done', result: CiResult }} PollStep
|
|
165
212
|
* @typedef {{ exec?: ExecFn, clock?: Clock, env?: NodeJS.ProcessEnv }} MainDeps
|
|
166
213
|
* @typedef {{ code: number, stdout: string }} Outcome
|
|
167
214
|
*/
|
|
@@ -224,14 +271,22 @@ function formatNames(names) {
|
|
|
224
271
|
}
|
|
225
272
|
|
|
226
273
|
/**
|
|
227
|
-
* D-CI-CLASSIFIER: check-ci-status step 5
|
|
228
|
-
* priority order: any `pending` is PENDING; else any `fail` or `cancel` is
|
|
229
|
-
* else every check `pass` or `skipping` with at least one `pass` is PASSING;
|
|
230
|
-
* INDETERMINATE. PASSING is a positive conjunction, so an undocumented bucket, an
|
|
231
|
-
* all-`skipping` set or an empty set never reaches it.
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
274
|
+
* D-CI-CLASSIFIER: check-ci-status step 5 over `bucket`, plus the stricter workflow
|
|
275
|
+
* rule. In priority order: any `pending` is PENDING; else any `fail` or `cancel` is
|
|
276
|
+
* FAILING; else every check `pass` or `skipping` with at least one `pass` is PASSING;
|
|
277
|
+
* else INDETERMINATE. PASSING is a positive conjunction, so an undocumented bucket, an
|
|
278
|
+
* all-`skipping` set or an empty set never reaches it.
|
|
279
|
+
*
|
|
280
|
+
* D-CI-WORKFLOW-RULE: a set that passes by step 5 is PASSING only when at least one
|
|
281
|
+
* passing check is a GitHub Actions job (a non-empty string `workflow`). A set with
|
|
282
|
+
* passes from external checks alone, a commit status or a third-party app, is
|
|
283
|
+
* INDETERMINATE (no-workflow-check): the repository's workflow may not have run. So
|
|
284
|
+
* PASSING proves that a GitHub Actions check passed and nothing is pending or failing;
|
|
285
|
+
* it does not prove that the test workflow ran by name. merge-readiness.test.ts runs
|
|
286
|
+
* every multiset of 1 to 3 checks through this and through the prose clauses and
|
|
287
|
+
* requires the statuses to agree, or this to be stricter only by no-workflow-check.
|
|
288
|
+
* (The empty set is not classified here: an empty list is "no checks", which
|
|
289
|
+
* waitForCi settles before it classifies.)
|
|
235
290
|
*
|
|
236
291
|
* @param {readonly Check[]} checks
|
|
237
292
|
* @returns {Classification}
|
|
@@ -240,6 +295,7 @@ function classifyChecks(checks) {
|
|
|
240
295
|
const failing = [];
|
|
241
296
|
const pending = [];
|
|
242
297
|
let pass = 0;
|
|
298
|
+
let actionsPass = 0;
|
|
243
299
|
let unrecognised = 0;
|
|
244
300
|
const count = Math.min(Array.isArray(checks) ? checks.length : 0, MAX_CHECKS);
|
|
245
301
|
for (let i = 0; i < count; i++) {
|
|
@@ -248,14 +304,21 @@ function classifyChecks(checks) {
|
|
|
248
304
|
case 'pending': pending.push(check.name); break;
|
|
249
305
|
case 'fail':
|
|
250
306
|
case 'cancel': failing.push(check.name); break;
|
|
251
|
-
case 'pass':
|
|
307
|
+
case 'pass':
|
|
308
|
+
pass++;
|
|
309
|
+
if (typeof check.workflow === 'string' && check.workflow !== '') actionsPass++;
|
|
310
|
+
break;
|
|
252
311
|
case 'skipping': break;
|
|
253
312
|
default: unrecognised++;
|
|
254
313
|
}
|
|
255
314
|
}
|
|
256
315
|
if (pending.length > 0) return { status: 'PENDING', reason: null, failing, pending };
|
|
257
316
|
if (failing.length > 0) return { status: 'FAILING', reason: null, failing, pending };
|
|
258
|
-
if (unrecognised === 0 && pass > 0)
|
|
317
|
+
if (unrecognised === 0 && pass > 0) {
|
|
318
|
+
return actionsPass > 0
|
|
319
|
+
? { status: 'PASSING', reason: null, failing, pending }
|
|
320
|
+
: { status: 'INDETERMINATE', reason: 'no-workflow-check', failing, pending };
|
|
321
|
+
}
|
|
259
322
|
return { status: 'INDETERMINATE', reason: 'unrecognised-bucket', failing, pending };
|
|
260
323
|
}
|
|
261
324
|
|
|
@@ -355,13 +418,20 @@ function parseJson(text) {
|
|
|
355
418
|
}
|
|
356
419
|
|
|
357
420
|
/**
|
|
421
|
+
* The `gh pr view --json headRefOid,mergeable,state` reply. `headRefOid` and `state` are
|
|
422
|
+
* strict: a head that is not 40 lowercase hex, or a `state` that is not a string, makes the
|
|
423
|
+
* reply unparseable. A `mergeable` that is absent or not a string is read as null, the same
|
|
424
|
+
* as UNKNOWN, and never makes the reply unparseable.
|
|
425
|
+
*
|
|
358
426
|
* @param {string} stdout
|
|
359
|
-
* @returns {string | null
|
|
427
|
+
* @returns {{ oid: string, mergeable: string | null, state: string } | null} null for any other shape
|
|
360
428
|
*/
|
|
361
|
-
function
|
|
429
|
+
function parseView(stdout) {
|
|
362
430
|
const value = parseJson(stdout);
|
|
363
431
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
364
|
-
|
|
432
|
+
if (typeof value.headRefOid !== 'string' || !HEAD_RE.test(value.headRefOid)) return null;
|
|
433
|
+
if (typeof value.state !== 'string') return null;
|
|
434
|
+
return { oid: value.headRefOid, mergeable: typeof value.mergeable === 'string' ? value.mergeable : null, state: value.state };
|
|
365
435
|
}
|
|
366
436
|
|
|
367
437
|
/**
|
|
@@ -374,11 +444,41 @@ function parseChecks(stdout) {
|
|
|
374
444
|
const checks = [];
|
|
375
445
|
for (const row of value) {
|
|
376
446
|
if (row === null || typeof row !== 'object' || typeof row.name !== 'string' || typeof row.bucket !== 'string') return null;
|
|
377
|
-
checks.push({ name: row.name, bucket: row.bucket });
|
|
447
|
+
checks.push({ name: row.name, bucket: row.bucket, workflow: typeof row.workflow === 'string' ? row.workflow : '' });
|
|
378
448
|
}
|
|
379
449
|
return checks;
|
|
380
450
|
}
|
|
381
451
|
|
|
452
|
+
/**
|
|
453
|
+
* D-CI-WORKFLOWS: whether the workflow list holds an active workflow file. A row is a
|
|
454
|
+
* workflow file when its `state` is exactly `active` and its `path` starts with
|
|
455
|
+
* `.github/workflows/`; the `dynamic/...` paths of GitHub's own apps and every
|
|
456
|
+
* disabled or deleted row are not. The reply is read whole before it is judged, so a
|
|
457
|
+
* row without a string `path` and `state` makes the reply unreadable even beside an
|
|
458
|
+
* active one. The list is one page: no match is a "no" only when the rows read equal
|
|
459
|
+
* `total_count` (and fit the page), and a page that is not all of the list proves
|
|
460
|
+
* nothing about the rest. The workflow files themselves are never opened.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} stdout the `gh api` reply
|
|
463
|
+
* @returns {boolean | null} true: some active workflow file. false: none, and the
|
|
464
|
+
* list is complete. null: bad JSON, a bad row, or an incomplete page.
|
|
465
|
+
*/
|
|
466
|
+
function parseActiveWorkflows(stdout) {
|
|
467
|
+
const value = parseJson(stdout);
|
|
468
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
469
|
+
const listed = value.workflows;
|
|
470
|
+
const total = value.total_count;
|
|
471
|
+
if (!Array.isArray(listed) || listed.length > WORKFLOWS_PAGE_ROWS) return null;
|
|
472
|
+
if (typeof total !== 'number' || !Number.isInteger(total) || total < 0) return null;
|
|
473
|
+
let active = false;
|
|
474
|
+
for (const row of listed) {
|
|
475
|
+
if (row === null || typeof row !== 'object' || typeof row.path !== 'string' || typeof row.state !== 'string') return null;
|
|
476
|
+
if (row.state === 'active' && row.path.startsWith(WORKFLOWS_DIR)) active = true;
|
|
477
|
+
}
|
|
478
|
+
if (active) return true;
|
|
479
|
+
return listed.length === total ? false : null;
|
|
480
|
+
}
|
|
481
|
+
|
|
382
482
|
// ---------------------------------------------------------------------------
|
|
383
483
|
// The wait
|
|
384
484
|
// ---------------------------------------------------------------------------
|
|
@@ -399,23 +499,171 @@ function settle(firstAt, now, settleMs) {
|
|
|
399
499
|
return { firstAt: opened, done: now - opened >= settleMs };
|
|
400
500
|
}
|
|
401
501
|
|
|
502
|
+
/**
|
|
503
|
+
* D-CI-MERGE-STATE: whether GitHub reports the PR as conflicting. It is the one rule for a
|
|
504
|
+
* conflict. mergeGate answers 'conflict' by it, and a caller that has read only the merge
|
|
505
|
+
* state asks it before it reads any check.
|
|
506
|
+
*
|
|
507
|
+
* @param {unknown} mergeable the `gh pr view` value in any shape
|
|
508
|
+
* @returns {boolean}
|
|
509
|
+
*/
|
|
510
|
+
function isConflicting(mergeable) {
|
|
511
|
+
return mergeable === 'CONFLICTING';
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* D-CI-MERGE-STATE, D-CI-WORKFLOWS: the one merge rule. GitHub computes `mergeable`
|
|
516
|
+
* lazily, and runs no pull request workflow on a conflicting PR, so the checks of a head
|
|
517
|
+
* say nothing about the PR unless GitHub reports it MERGEABLE. An empty check list says
|
|
518
|
+
* nothing either unless the repository has no workflow that could still start.
|
|
519
|
+
* `status` is a classifier status ('NO_CI' for an empty list), or null before any check
|
|
520
|
+
* is read; `mergeable` is the `gh pr view` value in any shape; `workflowsActive` is what
|
|
521
|
+
* the workflow probe found: true (an active workflow file), false (none), or null or
|
|
522
|
+
* left out when it was not made or gave no answer. Each case has its own answer, so a
|
|
523
|
+
* caller never reads `mergeable` or `workflowsActive` again:
|
|
524
|
+
* 'conflict' isConflicting(mergeable), whatever the status.
|
|
525
|
+
* 'await-merge-state' PASSING or NO_CI while mergeable is anything but MERGEABLE
|
|
526
|
+
* (UNKNOWN, absent or odd). It is neither a verdict nor a conflict:
|
|
527
|
+
* waitForCi keeps the wait open for another read, and a single read
|
|
528
|
+
* takes it as unknown.
|
|
529
|
+
* 'probe' NO_CI on MERGEABLE with no usable probe answer (null, left out or
|
|
530
|
+
* not a boolean): the workflow list has to be asked.
|
|
531
|
+
* 'workflow-active' NO_CI on MERGEABLE, and the probe found an active workflow file:
|
|
532
|
+
* a run may still start.
|
|
533
|
+
* 'keep' the status stands: PASSING on MERGEABLE, NO_CI on MERGEABLE with a
|
|
534
|
+
* probe that found no workflow file, and FAILING, PENDING and
|
|
535
|
+
* INDETERMINATE, which do not depend on the merge state or the probe.
|
|
536
|
+
*
|
|
537
|
+
* @param {string | null} status
|
|
538
|
+
* @param {unknown} mergeable
|
|
539
|
+
* @param {boolean | null} [workflowsActive]
|
|
540
|
+
* @returns {'conflict' | 'await-merge-state' | 'probe' | 'workflow-active' | 'keep'}
|
|
541
|
+
*/
|
|
542
|
+
function mergeGate(status, mergeable, workflowsActive) {
|
|
543
|
+
if (isConflicting(mergeable)) return 'conflict';
|
|
544
|
+
const mergeableNow = mergeable === 'MERGEABLE';
|
|
545
|
+
if (status === 'PASSING') return mergeableNow ? 'keep' : 'await-merge-state';
|
|
546
|
+
if (status !== 'NO_CI') return 'keep';
|
|
547
|
+
if (!mergeableNow) return 'await-merge-state';
|
|
548
|
+
if (workflowsActive === true) return 'workflow-active';
|
|
549
|
+
return workflowsActive === false ? 'keep' : 'probe';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @param {string} status
|
|
554
|
+
* @param {string | null} reason
|
|
555
|
+
* @param {string[]} [failing]
|
|
556
|
+
* @param {string[]} [pending]
|
|
557
|
+
* @returns {{ state: 'done', status: string, reason: string | null, failing: string[], pending: string[] }}
|
|
558
|
+
*/
|
|
559
|
+
function done(status, reason, failing = [], pending = []) {
|
|
560
|
+
return { state: 'done', status, reason, failing, pending };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* The outcome of one poll on the named head, as a pure function of what the poll read
|
|
565
|
+
* (the rules are classifyChecks's and mergeGate's). `verdict` is the classification
|
|
566
|
+
* of the rows, or null for an empty list. `windowDone` is whether the settle window has
|
|
567
|
+
* run out; it only matters for an empty list and for rows that are all external
|
|
568
|
+
* (no-workflow-check), the two cases a late GitHub Actions run can still change.
|
|
569
|
+
* `workflowsActive` is the probe's answer (see mergeGate) and matters for an empty list
|
|
570
|
+
* only. This function never reads `mergeable` or `workflowsActive` itself: each answer of
|
|
571
|
+
* mergeGate maps to one step. The outcome is one of:
|
|
572
|
+
* pending keep waiting: a pending check, or an unsettled window
|
|
573
|
+
* open a pass or an empty list while the gate awaits the merge state
|
|
574
|
+
* probe an empty list, window done, and the gate asks for the probe: the caller makes
|
|
575
|
+
* the workflow probe, then asks again with its answer
|
|
576
|
+
* done a final status and reason, with the failing and pending names
|
|
577
|
+
*
|
|
578
|
+
* @param {Classification | null} verdict
|
|
579
|
+
* @param {unknown} mergeable
|
|
580
|
+
* @param {boolean} windowDone
|
|
581
|
+
* @param {boolean | null} [workflowsActive]
|
|
582
|
+
* @returns {{ state: 'pending', pending: string[] } | { state: 'open' } | { state: 'probe' }
|
|
583
|
+
* | ReturnType<typeof done>}
|
|
584
|
+
*/
|
|
585
|
+
function matchedStep(verdict, mergeable, windowDone, workflowsActive) {
|
|
586
|
+
const gate = mergeGate(verdict === null ? 'NO_CI' : verdict.status, mergeable, workflowsActive);
|
|
587
|
+
if (gate === 'conflict') return done('INDETERMINATE', 'merge-conflict');
|
|
588
|
+
if (verdict === null) {
|
|
589
|
+
if (!windowDone) return { state: 'pending', pending: [] };
|
|
590
|
+
if (gate === 'keep') return done('NO_CI', null);
|
|
591
|
+
if (gate === 'workflow-active') return done('INDETERMINATE', 'no-workflow-check');
|
|
592
|
+
if (gate === 'probe') return { state: 'probe' };
|
|
593
|
+
return { state: 'open' };
|
|
594
|
+
}
|
|
595
|
+
if (verdict.reason === 'no-workflow-check') {
|
|
596
|
+
return windowDone ? done('INDETERMINATE', 'no-workflow-check') : { state: 'pending', pending: [] };
|
|
597
|
+
}
|
|
598
|
+
if (verdict.status === 'PENDING') return { state: 'pending', pending: verdict.pending };
|
|
599
|
+
if (gate !== 'keep') return { state: 'open' };
|
|
600
|
+
return done(verdict.status, verdict.reason, verdict.failing, verdict.pending);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* D-CI-WORKFLOWS: the workflow probe, as one result. `gh` is waitForCi's bounded gh
|
|
605
|
+
* call, so the probe gets its timeout and, with no time left, no call at all. The result is
|
|
606
|
+
* one of:
|
|
607
|
+
* deadline no time was left, so no call was made
|
|
608
|
+
* failed the call was not answered or exited non-zero: a failed poll
|
|
609
|
+
* unparseable the reply cannot be read, or is one page of many (parseActiveWorkflows)
|
|
610
|
+
* answer `active`: true for some active workflow file, false for none
|
|
611
|
+
*
|
|
612
|
+
* @param {(args: readonly string[]) => Promise<GhAnswer | null>} gh
|
|
613
|
+
* @returns {Promise<{ kind: 'deadline' } | { kind: 'failed' } | { kind: 'unparseable' }
|
|
614
|
+
* | { kind: 'answer', active: boolean }>}
|
|
615
|
+
*/
|
|
616
|
+
async function askWorkflows(gh) {
|
|
617
|
+
const probe = await gh(WORKFLOWS_ARGS);
|
|
618
|
+
if (probe === null) return { kind: 'deadline' };
|
|
619
|
+
if (!probe.answered || probe.status !== 0) return { kind: 'failed' };
|
|
620
|
+
const active = parseActiveWorkflows(probe.stdout);
|
|
621
|
+
return active === null ? { kind: 'unparseable' } : { kind: 'answer', active };
|
|
622
|
+
}
|
|
623
|
+
|
|
402
624
|
/**
|
|
403
625
|
* D-CI-HEAD-BINDING: wait for the CI verdict of `head`, and of nothing else. `gh pr
|
|
404
626
|
* checks` has no SHA field and reports the PR's current head, so each poll reads
|
|
405
627
|
* `headRefOid` first. A different value means the checks on offer belong to another
|
|
406
|
-
* head: they are not read, and the poll is PENDING until
|
|
407
|
-
* passed since the first mismatch (GitHub moves the head
|
|
408
|
-
* seconds after a push), then INDETERMINATE (head-mismatch).
|
|
409
|
-
* the mismatch clock.
|
|
410
|
-
*
|
|
628
|
+
* head: they are not read, the merge state is not used, and the poll is PENDING until
|
|
629
|
+
* SETTLE_WINDOW_SECONDS have passed since the first mismatch (GitHub moves the head
|
|
630
|
+
* and creates checks a few seconds after a push), then INDETERMINATE (head-mismatch).
|
|
631
|
+
* A matching head clears the mismatch clock. The poll reads `headRefOid` once more after
|
|
632
|
+
* the checks call: the checks count only when it is still `head`. A head that moved
|
|
633
|
+
* leaves the poll open (no verdict, no settle clock moved); a second read that fails or
|
|
634
|
+
* cannot be read is a failed poll. A PASSING needs a poll that saw this head before and
|
|
635
|
+
* after its checks and passed the rules of classifyChecks (D-CI-WORKFLOW-RULE) and
|
|
636
|
+
* mergeGate (D-CI-MERGE-STATE).
|
|
411
637
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
638
|
+
* D-CI-PR-STATE: GitHub reports `mergeable` UNKNOWN for a merged PR for good (gh 2.88.1,
|
|
639
|
+
* probed on 2026-10-22), so a PR that is no longer open would wait to the deadline for a
|
|
640
|
+
* merge state that never comes. The first read of a poll also asks `state`: anything but
|
|
641
|
+
* the exact string OPEN ends the run at once as INDETERMINATE (not-open), before the head
|
|
642
|
+
* is compared, and a `state` that is not a string is unparseable. Never PASSING.
|
|
643
|
+
*
|
|
644
|
+
* D-CI-MERGE-STATE, D-CI-WORKFLOWS: the rules are mergeGate's; this loop acts on its
|
|
645
|
+
* answers. On a matching head, `mergeable` CONFLICTING is INDETERMINATE (merge-conflict)
|
|
646
|
+
* at once, before the checks are read. An open step (the gate awaits the merge state) is
|
|
647
|
+
* not PENDING, so a head that never reads MERGEABLE ends INDETERMINATE (deadline). Rows
|
|
648
|
+
* that are all external checks share the empty list's settle window, which a pending or
|
|
649
|
+
* failing row clears, and end INDETERMINATE (no-workflow-check) when it runs out. A step
|
|
650
|
+
* that asks for the probe gets it from askWorkflows, `gh api` over WORKFLOWS_ARGS,
|
|
651
|
+
* through the same bounded gh call (timeout min(GH_CALL_CAP_SECONDS, time left), none
|
|
652
|
+
* when no time is left, which ends the loop as for any call). At most one probe is
|
|
653
|
+
* answered per run: an answered probe ends the run in that poll, so no later poll reads
|
|
654
|
+
* it and no settle() clock runs for it. A probe whose gh call fails is a failed poll, and
|
|
655
|
+
* the next poll probes again; a reply parseActiveWorkflows cannot read is INDETERMINATE
|
|
656
|
+
* (unparseable). No other path makes the probe.
|
|
657
|
+
*
|
|
658
|
+
* Per poll: `gh pr view <n> --json headRefOid,mergeable,state`, then `gh pr checks <n>
|
|
659
|
+
* --json name,state,bucket,workflow` (exit 0 or 8 are results, as in check-ci-status
|
|
660
|
+
* step 3), then the same view call again (a bounded gh call like the others, not made
|
|
661
|
+
* when no time is left), then, on that one path, the workflow probe. A PENDING or open
|
|
662
|
+
* poll sleeps min(POLL_INTERVAL_SECONDS, remaining) and polls again; any other result
|
|
663
|
+
* returns. A poll whose gh call fails is not a result: three failed polls in a row are
|
|
664
|
+
* INDETERMINATE (gh-failed), fewer are retried. The loop stops at the deadline or after
|
|
665
|
+
* MAX_POLLS with its last state: PENDING stays PENDING, and anything unknown, an open
|
|
666
|
+
* step included, becomes INDETERMINATE (deadline).
|
|
419
667
|
*
|
|
420
668
|
* It never throws: an exec that throws is a gh failure.
|
|
421
669
|
*
|
|
@@ -466,68 +714,110 @@ async function waitForCi(input) {
|
|
|
466
714
|
return normalize(res);
|
|
467
715
|
};
|
|
468
716
|
|
|
469
|
-
let failedPolls = 0;
|
|
470
717
|
let firstMismatchAt = null;
|
|
471
718
|
let firstEmptyAt = null;
|
|
472
|
-
/** @type {{ status: string, pending: string[] }} */
|
|
473
|
-
let last = { status: 'INDETERMINATE', pending: [] };
|
|
474
719
|
|
|
475
|
-
|
|
476
|
-
|
|
720
|
+
/**
|
|
721
|
+
* A step that ends the run with a result.
|
|
722
|
+
*
|
|
723
|
+
* @param {string} status
|
|
724
|
+
* @param {string | null} reason
|
|
725
|
+
* @param {string[]} failing
|
|
726
|
+
* @param {string[]} pending
|
|
727
|
+
* @returns {PollStep}
|
|
728
|
+
*/
|
|
729
|
+
const resultStep = (status, reason, failing, pending) => ({ state: 'done', result: finish(status, reason, failing, pending) });
|
|
477
730
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
731
|
+
/**
|
|
732
|
+
* The step for what matchedStep decided. A probe request cannot be a final step: it is
|
|
733
|
+
* no verdict, and the wait stays open.
|
|
734
|
+
*
|
|
735
|
+
* @param {ReturnType<typeof matchedStep>} next
|
|
736
|
+
* @returns {PollStep}
|
|
737
|
+
*/
|
|
738
|
+
const stepOf = next => {
|
|
739
|
+
if (next.state === 'done') return resultStep(next.status, next.reason, next.failing, next.pending);
|
|
740
|
+
return next.state === 'probe' ? { state: 'open' } : next;
|
|
741
|
+
};
|
|
481
742
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
743
|
+
const viewArgs = ['pr', 'view', pr, '--json', VIEW_FIELDS];
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* One poll: the view, then, on a matching head that does not conflict, the checks, then
|
|
747
|
+
* the view again, then, on the one path that needs it, the workflow probe. Both settle
|
|
748
|
+
* windows run here, after the second view, so a poll that does not count moves neither.
|
|
749
|
+
*
|
|
750
|
+
* @returns {Promise<PollStep>}
|
|
751
|
+
*/
|
|
752
|
+
const readPoll = async () => {
|
|
753
|
+
const view = await gh(viewArgs);
|
|
754
|
+
if (view === null) return { state: 'deadline' };
|
|
755
|
+
if (view.answered && view.status === 1 && NO_PR_RE.test(view.stderr)) return resultStep('NO_PR', null, [], []);
|
|
756
|
+
if (!view.answered || view.status !== 0) return { state: 'failed' };
|
|
757
|
+
const parsed = parseView(view.stdout);
|
|
758
|
+
if (parsed === null) return resultStep('INDETERMINATE', 'unparseable', [], []);
|
|
759
|
+
if (parsed.state !== 'OPEN') return resultStep('INDETERMINATE', 'not-open', [], []);
|
|
760
|
+
if (parsed.oid !== head) {
|
|
761
|
+
const mismatch = settle(firstMismatchAt, clock.now(), settleMs);
|
|
762
|
+
firstMismatchAt = mismatch.firstAt;
|
|
763
|
+
return mismatch.done ? resultStep('INDETERMINATE', 'head-mismatch', [], []) : { state: 'pending', pending: [] };
|
|
764
|
+
}
|
|
765
|
+
firstMismatchAt = null;
|
|
766
|
+
if (isConflicting(parsed.mergeable)) return resultStep('INDETERMINATE', 'merge-conflict', [], []);
|
|
767
|
+
|
|
768
|
+
const checks = await gh(['pr', 'checks', pr, '--json', 'name,state,bucket,workflow']);
|
|
769
|
+
if (checks === null) return { state: 'deadline' };
|
|
770
|
+
const isResult = checks.answered && (checks.status === 0 || checks.status === 8);
|
|
771
|
+
const isEmpty = checks.answered && checks.status === 1 && NO_CHECKS_RE.test(checks.stderr);
|
|
772
|
+
if (!isResult && !isEmpty) return { state: 'failed' };
|
|
773
|
+
const second = await gh(viewArgs);
|
|
774
|
+
if (second === null) return { state: 'deadline' };
|
|
775
|
+
const after = second.answered && second.status === 0 ? parseView(second.stdout) : null;
|
|
776
|
+
if (after === null) return { state: 'failed' };
|
|
777
|
+
if (after.oid !== head) return { state: 'open' };
|
|
778
|
+
const rows = isResult ? parseChecks(checks.stdout) : [];
|
|
779
|
+
if (rows === null) return resultStep('INDETERMINATE', 'unparseable', [], []);
|
|
780
|
+
const verdict = rows.length > 0 ? classifyChecks(rows) : null;
|
|
781
|
+
let windowDone = false;
|
|
782
|
+
if (verdict === null || verdict.reason === 'no-workflow-check') {
|
|
783
|
+
const empty = settle(firstEmptyAt, clock.now(), settleMs);
|
|
784
|
+
firstEmptyAt = empty.firstAt;
|
|
785
|
+
windowDone = empty.done;
|
|
486
786
|
} else {
|
|
487
|
-
|
|
488
|
-
if (oid === null) return finish('INDETERMINATE', 'unparseable', [], []);
|
|
489
|
-
if (oid !== head) {
|
|
490
|
-
const mismatch = settle(firstMismatchAt, clock.now(), settleMs);
|
|
491
|
-
firstMismatchAt = mismatch.firstAt;
|
|
492
|
-
step = mismatch.done
|
|
493
|
-
? { state: 'done', result: finish('INDETERMINATE', 'head-mismatch', [], []) }
|
|
494
|
-
: { state: 'pending', pending: [] };
|
|
495
|
-
} else {
|
|
496
|
-
firstMismatchAt = null;
|
|
497
|
-
const checks = await gh(['pr', 'checks', pr, '--json', 'name,state,bucket']);
|
|
498
|
-
if (checks === null) break;
|
|
499
|
-
const isResult = checks.answered && (checks.status === 0 || checks.status === 8);
|
|
500
|
-
const isEmpty = checks.answered && checks.status === 1 && NO_CHECKS_RE.test(checks.stderr);
|
|
501
|
-
if (!isResult && !isEmpty) {
|
|
502
|
-
step = { state: 'failed' };
|
|
503
|
-
} else {
|
|
504
|
-
const rows = isResult ? parseChecks(checks.stdout) : [];
|
|
505
|
-
if (rows === null) return finish('INDETERMINATE', 'unparseable', [], []);
|
|
506
|
-
if (rows.length === 0) {
|
|
507
|
-
const empty = settle(firstEmptyAt, clock.now(), settleMs);
|
|
508
|
-
firstEmptyAt = empty.firstAt;
|
|
509
|
-
step = empty.done
|
|
510
|
-
? { state: 'done', result: finish('NO_CI', null, [], []) }
|
|
511
|
-
: { state: 'pending', pending: [] };
|
|
512
|
-
} else {
|
|
513
|
-
firstEmptyAt = null;
|
|
514
|
-
const verdict = classifyChecks(rows);
|
|
515
|
-
step = verdict.status === 'PENDING'
|
|
516
|
-
? { state: 'pending', pending: verdict.pending }
|
|
517
|
-
: { state: 'done', result: finish(verdict.status, verdict.reason, verdict.failing, verdict.pending) };
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
787
|
+
firstEmptyAt = null;
|
|
521
788
|
}
|
|
522
789
|
|
|
790
|
+
const next = matchedStep(verdict, parsed.mergeable, windowDone, null);
|
|
791
|
+
if (next.state !== 'probe') return stepOf(next);
|
|
792
|
+
const asked = await askWorkflows(gh);
|
|
793
|
+
if (asked.kind === 'deadline') return { state: 'deadline' };
|
|
794
|
+
if (asked.kind === 'failed') return { state: 'failed' };
|
|
795
|
+
if (asked.kind === 'unparseable') return resultStep('INDETERMINATE', 'unparseable', [], []);
|
|
796
|
+
return stepOf(matchedStep(verdict, parsed.mergeable, windowDone, asked.active));
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
let failedPolls = 0;
|
|
800
|
+
/** @type {{ status: string, pending: string[] }} */
|
|
801
|
+
let last = { status: 'INDETERMINATE', pending: [] };
|
|
802
|
+
|
|
803
|
+
for (let poll = 0; poll < MAX_POLLS; poll++) {
|
|
804
|
+
if (clock.now() >= deadline) break;
|
|
805
|
+
|
|
806
|
+
const step = await readPoll();
|
|
807
|
+
if (step.state === 'deadline') break;
|
|
523
808
|
if (step.state === 'done') return step.result;
|
|
524
809
|
if (step.state === 'failed') {
|
|
525
810
|
failedPolls++;
|
|
526
811
|
if (failedPolls >= MAX_CONSECUTIVE_GH_FAILURES) return finish('INDETERMINATE', 'gh-failed', [], []);
|
|
527
812
|
last = { status: 'INDETERMINATE', pending: [] };
|
|
528
|
-
} else {
|
|
813
|
+
} else if (step.state === 'pending') {
|
|
529
814
|
failedPolls = 0;
|
|
530
815
|
last = { status: 'PENDING', pending: step.pending };
|
|
816
|
+
} else {
|
|
817
|
+
// An open step: no verdict, and the deadline exit after it is INDETERMINATE, never
|
|
818
|
+
// PENDING.
|
|
819
|
+
failedPolls = 0;
|
|
820
|
+
last = { status: 'INDETERMINATE', pending: [] };
|
|
531
821
|
}
|
|
532
822
|
|
|
533
823
|
const left = deadline - clock.now();
|
|
@@ -643,10 +933,17 @@ module.exports = Object.freeze({
|
|
|
643
933
|
NAMES_LISTED,
|
|
644
934
|
LINE_RE,
|
|
645
935
|
USAGE_LINE,
|
|
936
|
+
WORKFLOWS_ARGS,
|
|
646
937
|
parseArgs,
|
|
647
938
|
sanitizeName,
|
|
648
939
|
formatNames,
|
|
649
940
|
classifyChecks,
|
|
941
|
+
isConflicting,
|
|
942
|
+
mergeGate,
|
|
943
|
+
matchedStep,
|
|
944
|
+
parseChecks,
|
|
945
|
+
parseActiveWorkflows,
|
|
946
|
+
askWorkflows,
|
|
650
947
|
renderLine,
|
|
651
948
|
checkLine,
|
|
652
949
|
waitForCi,
|