pr-shepherd 0.32.1 → 0.32.3
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/.claude-plugin/plugin.json +1 -1
- package/bin/checks/classify.mjs +33 -5
- package/bin/checks/superseded.mjs +59 -0
- package/bin/cli/duration-flag.mjs +7 -27
- package/bin/cli/exit-codes.mjs +7 -16
- package/bin/cli/handlers.mjs +1 -1
- package/bin/cli/help-command-pages.mjs +11 -7
- package/bin/cli/help-top-page.mjs +5 -5
- package/bin/cli/iterate-flags.mjs +10 -7
- package/bin/cli/iterate-formatter.mjs +11 -1
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/poll-handler.mjs +1 -1
- package/bin/commands/check.mjs +1 -0
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/escalate.mjs +16 -4
- package/bin/commands/iterate/helpers.mjs +37 -0
- package/bin/commands/iterate/index.mjs +3 -26
- package/bin/commands/iterate/stall.mjs +5 -5
- package/bin/github/batch-parser-helpers.mjs +37 -2
- package/bin/github/batch-parsers.mjs +2 -29
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/types/check-classification.mjs +2 -0
- package/bin/types/github.mjs +2 -0
- package/bin/types.mjs +1 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
package/bin/checks/classify.mjs
CHANGED
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
* to PR readiness.
|
|
8
8
|
* 2. Drop checks with `conclusion == SKIPPED` or `conclusion == NEUTRAL` from the
|
|
9
9
|
* pass/fail tally. Report them as "skipped" for transparency but don't block on them.
|
|
10
|
+
* 3. Reclassify `CANCELLED` checks as "superseded" (non-blocking) when a newer run of
|
|
11
|
+
* the same workflow exists on the same commit — this is GitHub's concurrency-group
|
|
12
|
+
* eviction behavior, not a real failure. GitHub branch protection itself resolves
|
|
13
|
+
* required status checks by latest-run-per-name and merges past these; mirroring
|
|
14
|
+
* that here keeps shepherd's verdict aligned with what GitHub will actually allow.
|
|
10
15
|
*/
|
|
11
16
|
import { loadConfig } from "../config/load.mjs";
|
|
17
|
+
import { buildSupersededIndices } from "./superseded.mjs";
|
|
12
18
|
import picomatch from "picomatch";
|
|
13
19
|
/**
|
|
14
20
|
* Classify a list of raw check runs into shepherd categories.
|
|
@@ -22,9 +28,19 @@ export function classifyChecks(checks) {
|
|
|
22
28
|
const isIgnored = buildMatcher(config.ignoreChecks ?? []);
|
|
23
29
|
const isProtected = buildMatcher(config.actions.neverCancelRuns ?? []);
|
|
24
30
|
const protectedRunIds = buildProtectedRunIds(checks, isProtected);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
const supersededIndices = buildSupersededIndices(checks);
|
|
32
|
+
return checks.map((c, index) => {
|
|
33
|
+
if (isIgnored(c.name) && !isProtectedCheck(c, protectedRunIds)) {
|
|
34
|
+
return { ...c, category: "ignored" };
|
|
35
|
+
}
|
|
36
|
+
const classified = classify(c, relevantEvents);
|
|
37
|
+
// Only ever override a "failing" verdict (i.e. conclusion === CANCELLED, guaranteed by
|
|
38
|
+
// buildSupersededIndices below) — never touch filtered/skipped/passed classifications.
|
|
39
|
+
if (classified.category === "failing" && supersededIndices.has(index)) {
|
|
40
|
+
return { ...classified, category: "superseded" };
|
|
41
|
+
}
|
|
42
|
+
return classified;
|
|
43
|
+
});
|
|
28
44
|
}
|
|
29
45
|
function buildMatcher(patterns) {
|
|
30
46
|
if (patterns.length === 0)
|
|
@@ -74,7 +90,10 @@ function classify(check, relevantEvents) {
|
|
|
74
90
|
}
|
|
75
91
|
/** Compute a high-level CI verdict from a list of classified checks. */
|
|
76
92
|
export function getCiVerdict(classified) {
|
|
77
|
-
const relevant = classified.filter((c) => c.category !== "filtered" &&
|
|
93
|
+
const relevant = classified.filter((c) => c.category !== "filtered" &&
|
|
94
|
+
c.category !== "skipped" &&
|
|
95
|
+
c.category !== "ignored" &&
|
|
96
|
+
c.category !== "superseded");
|
|
78
97
|
const anyInProgress = relevant.some((c) => c.category === "in_progress");
|
|
79
98
|
const anyFailing = relevant.some((c) => c.category === "failing");
|
|
80
99
|
// When there are no relevant checks (e.g. docs-only PR where all checks are filtered/skipped),
|
|
@@ -83,5 +102,14 @@ export function getCiVerdict(classified) {
|
|
|
83
102
|
const hasChecks = relevant.length > 0;
|
|
84
103
|
const filteredNames = classified.filter((c) => c.category === "filtered").map((c) => c.name);
|
|
85
104
|
const ignoredNames = Array.from(new Set(classified.filter((c) => c.category === "ignored").map((c) => c.name)));
|
|
86
|
-
|
|
105
|
+
const supersededNames = Array.from(new Set(classified.filter((c) => c.category === "superseded").map((c) => c.name)));
|
|
106
|
+
return {
|
|
107
|
+
allPassed,
|
|
108
|
+
hasChecks,
|
|
109
|
+
anyInProgress,
|
|
110
|
+
anyFailing,
|
|
111
|
+
filteredNames,
|
|
112
|
+
ignoredNames,
|
|
113
|
+
supersededNames,
|
|
114
|
+
};
|
|
87
115
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects check runs that are `CANCELLED` because a newer run of the *same workflow*
|
|
3
|
+
* superseded them on the same commit (concurrency-group eviction), rather than a genuine
|
|
4
|
+
* cancellation. Split out of classify.mts to stay under the file-length cap.
|
|
5
|
+
*/
|
|
6
|
+
/** Grouping key for a check's workflow: numeric `workflowId`, falling back to `workflowName`. */
|
|
7
|
+
function workflowKeyOf(check) {
|
|
8
|
+
return check.workflowId ?? check.workflowName;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Grouping key is `workflowId ?? workflowName` — the numeric GitHub Actions workflow database
|
|
12
|
+
* ID when available, falling back to the display name. Checks with neither a workflow identity
|
|
13
|
+
* nor a numeric `runId` (status contexts, startup-failure synthetics) never participate: they
|
|
14
|
+
* can neither be marked superseded nor count as evidence of a newer run.
|
|
15
|
+
*
|
|
16
|
+
* A check is superseded iff its own conclusion is `CANCELLED` and some other check sharing its
|
|
17
|
+
* workflow key has a strictly greater `runId`. The newest run for a workflow is therefore never
|
|
18
|
+
* superseded, even if it is itself cancelled — that case stays "failing" so the agent can decide
|
|
19
|
+
* whether to rerun it.
|
|
20
|
+
*
|
|
21
|
+
* @returns Indices into `checks` (not object identities, since check-run objects are not
|
|
22
|
+
* deduplicated by reference elsewhere) that should be reclassified as "superseded".
|
|
23
|
+
*/
|
|
24
|
+
export function buildSupersededIndices(checks) {
|
|
25
|
+
const runIdByIndex = new Map();
|
|
26
|
+
const maxRunIdByWorkflow = new Map();
|
|
27
|
+
checks.forEach((check, index) => {
|
|
28
|
+
const workflowKey = workflowKeyOf(check);
|
|
29
|
+
if (workflowKey === undefined || check.runId === null)
|
|
30
|
+
return;
|
|
31
|
+
const runIdNum = Number(check.runId);
|
|
32
|
+
if (!Number.isFinite(runIdNum))
|
|
33
|
+
return;
|
|
34
|
+
runIdByIndex.set(index, runIdNum);
|
|
35
|
+
const currentMax = maxRunIdByWorkflow.get(workflowKey);
|
|
36
|
+
if (currentMax === undefined || runIdNum > currentMax) {
|
|
37
|
+
maxRunIdByWorkflow.set(workflowKey, runIdNum);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const superseded = new Set();
|
|
41
|
+
checks.forEach((check, index) => {
|
|
42
|
+
if (check.conclusion !== "CANCELLED")
|
|
43
|
+
return;
|
|
44
|
+
const runIdNum = runIdByIndex.get(index);
|
|
45
|
+
if (runIdNum === undefined)
|
|
46
|
+
return;
|
|
47
|
+
// workflowKeyOf(check) is guaranteed defined here, with a corresponding entry in
|
|
48
|
+
// maxRunIdByWorkflow: runIdByIndex is only ever populated in the loop above alongside a
|
|
49
|
+
// maxRunIdByWorkflow entry for that same workflow key (at minimum, this check's own
|
|
50
|
+
// runIdNum) — the two maps are always updated together for a given index. A defensive
|
|
51
|
+
// undefined-check here would therefore guard a branch no input can ever exercise, which
|
|
52
|
+
// would silently fail this repo's 100%-coverage requirement instead of catching a real bug.
|
|
53
|
+
const maxRunId = maxRunIdByWorkflow.get(workflowKeyOf(check));
|
|
54
|
+
if (maxRunId > runIdNum) {
|
|
55
|
+
superseded.add(index);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return superseded;
|
|
59
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { parseSecondsDurationParts } from "./exit-codes.mjs";
|
|
2
|
-
export function validateSecondsDurationFlag(command, flag, value, presentAsSeparateArg) {
|
|
2
|
+
export function validateSecondsDurationFlag(command, flag, value, presentAsSeparateArg, opts = {}) {
|
|
3
|
+
const bareUnit = opts.defaultUnit === "m" ? "minutes" : "seconds";
|
|
4
|
+
const example = opts.defaultUnit === "m" ? "15m" : "30s";
|
|
3
5
|
if (value === null) {
|
|
4
6
|
if (presentAsSeparateArg) {
|
|
5
|
-
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag}
|
|
7
|
+
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} ${example})\n`);
|
|
6
8
|
process.exitCode = 1;
|
|
7
9
|
return null;
|
|
8
10
|
}
|
|
@@ -10,34 +12,12 @@ export function validateSecondsDurationFlag(command, flag, value, presentAsSepar
|
|
|
10
12
|
}
|
|
11
13
|
const trimmed = value.trim();
|
|
12
14
|
if (trimmed.startsWith("--")) {
|
|
13
|
-
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag}
|
|
15
|
+
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} ${example})\n`);
|
|
14
16
|
process.exitCode = 1;
|
|
15
17
|
return null;
|
|
16
18
|
}
|
|
17
|
-
if (!parseSecondsDurationParts(trimmed)) {
|
|
18
|
-
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 30s, 4.5m, 1h, or bare
|
|
19
|
-
process.exitCode = 1;
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
return trimmed;
|
|
23
|
-
}
|
|
24
|
-
export function validateDurationFlag(command, flag, value, presentAsSeparateArg) {
|
|
25
|
-
if (value === null) {
|
|
26
|
-
if (presentAsSeparateArg) {
|
|
27
|
-
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} 15m)\n`);
|
|
28
|
-
process.exitCode = 1;
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
const trimmed = value.trim();
|
|
34
|
-
if (trimmed.startsWith("--")) {
|
|
35
|
-
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} 15m)\n`);
|
|
36
|
-
process.exitCode = 1;
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
if (!/^\d+(?:m|min|minutes?|h|hours?)?$/.test(trimmed)) {
|
|
40
|
-
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 5m, 2h, 10m, or 1h.\n`);
|
|
19
|
+
if (!parseSecondsDurationParts(trimmed, opts)) {
|
|
20
|
+
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 30s, 4.5m, 1h, or a bare number (${bareUnit}).\n`);
|
|
41
21
|
process.exitCode = 1;
|
|
42
22
|
return null;
|
|
43
23
|
}
|
package/bin/cli/exit-codes.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { loadConfig } from "../config/load.mjs";
|
|
2
1
|
const SECOND_DURATION_UNITS = new Set([
|
|
3
2
|
"s",
|
|
4
3
|
"sec",
|
|
@@ -12,17 +11,7 @@ const SECOND_DURATION_UNITS = new Set([
|
|
|
12
11
|
"hour",
|
|
13
12
|
"hours",
|
|
14
13
|
]);
|
|
15
|
-
export function
|
|
16
|
-
const m = /^(\d+)(m|min|minutes?|h|hours?)?$/.exec(s.trim());
|
|
17
|
-
if (!m)
|
|
18
|
-
return defaultMinutes ?? loadConfig().watch.readyDelayMinutes;
|
|
19
|
-
const n = parseInt(m[1], 10);
|
|
20
|
-
const unit = m[2] ?? "m";
|
|
21
|
-
if (unit.startsWith("h"))
|
|
22
|
-
return n * 60;
|
|
23
|
-
return n;
|
|
24
|
-
}
|
|
25
|
-
export function parseSecondsDurationParts(s) {
|
|
14
|
+
export function parseSecondsDurationParts(s, opts = {}) {
|
|
26
15
|
const trimmed = s.trim();
|
|
27
16
|
const match = /^(\d+(?:\.\d+)?)([a-z]+)?$/.exec(trimmed);
|
|
28
17
|
if (!match)
|
|
@@ -31,16 +20,18 @@ export function parseSecondsDurationParts(s) {
|
|
|
31
20
|
const explicitUnit = match[2];
|
|
32
21
|
if (!amount || (amount.includes(".") && !explicitUnit))
|
|
33
22
|
return null;
|
|
34
|
-
const unit = explicitUnit ?? "s";
|
|
23
|
+
const unit = explicitUnit ?? opts.defaultUnit ?? "s";
|
|
35
24
|
if (!SECOND_DURATION_UNITS.has(unit))
|
|
36
25
|
return null;
|
|
37
26
|
const value = Number(amount);
|
|
38
|
-
if (!Number.isFinite(value)
|
|
27
|
+
if (!Number.isFinite(value))
|
|
28
|
+
return null;
|
|
29
|
+
if (opts.allowZero ? value < 0 : value <= 0)
|
|
39
30
|
return null;
|
|
40
31
|
return { value, unit };
|
|
41
32
|
}
|
|
42
|
-
export function parseDurationToSeconds(s, defaultSeconds) {
|
|
43
|
-
const parsed = parseSecondsDurationParts(s);
|
|
33
|
+
export function parseDurationToSeconds(s, defaultSeconds, opts = {}) {
|
|
34
|
+
const parsed = parseSecondsDurationParts(s, opts);
|
|
44
35
|
if (!parsed)
|
|
45
36
|
return defaultSeconds;
|
|
46
37
|
if (parsed.unit.startsWith("h"))
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -94,7 +94,7 @@ export async function handleIterate(args) {
|
|
|
94
94
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
95
95
|
const cfg = loadConfig();
|
|
96
96
|
const flags = parseIterateFlags(extra, cfg);
|
|
97
|
-
if (flags.readyDelaySuffix === null)
|
|
97
|
+
if (flags.readyDelaySuffix === null || flags.stallTimeoutSuffix === null)
|
|
98
98
|
return;
|
|
99
99
|
const result = await runIterate({
|
|
100
100
|
...globalOpts,
|
|
@@ -80,14 +80,16 @@ Usage:
|
|
|
80
80
|
pr-shepherd iterate [PR] [iterate-flags]
|
|
81
81
|
|
|
82
82
|
Iterate flags:
|
|
83
|
-
--ready-delay <duration> Settle window before a clean PR cancels. Example: 15m.
|
|
84
|
-
--stall-timeout <duration> Escalate repeated unchanged failures after this duration.
|
|
83
|
+
--ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.
|
|
84
|
+
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
85
85
|
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
86
86
|
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
87
87
|
--format text|json Output Markdown text or JSON. Default: text.
|
|
88
88
|
--verbose Include verbose iterate fields.
|
|
89
89
|
--help, -h Print this help and exit before GitHub, git, config, or log I/O.
|
|
90
90
|
|
|
91
|
+
Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).
|
|
92
|
+
|
|
91
93
|
Actions:
|
|
92
94
|
WAIT No immediate code action; recheck later or use pr-shepherd poll.
|
|
93
95
|
MARK_READY Draft PR was marked ready for review.
|
|
@@ -110,21 +112,23 @@ Usage:
|
|
|
110
112
|
pr-shepherd poll [PR] [poll-flags] [iterate-flags]
|
|
111
113
|
|
|
112
114
|
Poll flags:
|
|
113
|
-
--interval <duration> Sleep between WAIT ticks. Default: 60s.
|
|
114
|
-
--timeout <duration> Maximum wall-clock wait. Default: 4.5m.
|
|
115
|
+
--interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: 60s.
|
|
116
|
+
--timeout <duration> Maximum wall-clock wait. Bare number = seconds. Default: 4.5m.
|
|
115
117
|
--quiet-status During WAIT polling, print only changed status snapshots.
|
|
116
118
|
--until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.
|
|
117
119
|
|
|
118
120
|
Forwarded iterate flags:
|
|
119
|
-
--ready-delay <duration> Settle window before a clean PR cancels. Example: 15m.
|
|
120
|
-
--stall-timeout <duration> Escalate repeated unchanged failures after this duration.
|
|
121
|
+
--ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.
|
|
122
|
+
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
121
123
|
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
122
124
|
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
123
125
|
--format text|json Output Markdown text or JSON. Default: text.
|
|
124
126
|
--verbose Include verbose iterate fields and detailed per-tick lines.
|
|
125
127
|
--help, -h Print this help and exit before GitHub, git, config, or log I/O.
|
|
126
128
|
|
|
127
|
-
Durations accept
|
|
129
|
+
Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds
|
|
130
|
+
for --interval/--timeout, minutes for --ready-delay/--stall-timeout); decimals are allowed only with
|
|
131
|
+
an explicit unit (4.5m).
|
|
128
132
|
Each WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.
|
|
129
133
|
With --until-terminal, --timeout is ignored for WAIT ticks and polling continues until FIX_CODE, CANCEL, or ESCALATE.
|
|
130
134
|
|
|
@@ -36,14 +36,14 @@ Common flags:
|
|
|
36
36
|
--help, -h Print help and exit before any GitHub, git, config, or log I/O.
|
|
37
37
|
|
|
38
38
|
Iterate flags:
|
|
39
|
-
--ready-delay <duration> Settle window before a clean PR cancels. Example: 15m.
|
|
40
|
-
--stall-timeout <duration> Escalate repeated unchanged failures after this duration.
|
|
39
|
+
--ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.
|
|
40
|
+
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
41
41
|
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
42
42
|
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
43
43
|
|
|
44
44
|
Poll flags:
|
|
45
|
-
--interval <duration> Delay between WAIT ticks. Default: 60s.
|
|
46
|
-
--timeout <duration> Poll wall-clock cap. Default: 4.5m.
|
|
45
|
+
--interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.
|
|
46
|
+
--timeout <duration> Poll wall-clock cap. Bare number = seconds. Default: 4.5m.
|
|
47
47
|
--quiet-status During WAIT polling, print only changed status snapshots.
|
|
48
48
|
--until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.
|
|
49
49
|
|
|
@@ -60,6 +60,6 @@ Exit codes for iterate and poll:
|
|
|
60
60
|
2 CANCEL
|
|
61
61
|
3 ESCALATE
|
|
62
62
|
|
|
63
|
-
Duration examples: 30s, 4.5m, 1h
|
|
63
|
+
Duration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).
|
|
64
64
|
|
|
65
65
|
Run 'pr-shepherd <command> --help' for command-specific details.`;
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { getFlag, hasFlag } from "./args.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { parseDurationToSeconds } from "./exit-codes.mjs";
|
|
3
|
+
import { validateSecondsDurationFlag } from "./duration-flag.mjs";
|
|
4
|
+
// --ready-delay and --stall-timeout are minute-family flags: a bare number means minutes, and 0 is a
|
|
5
|
+
// valid value (it disables the ready-delay settle window / stall-timeout escalation, respectively).
|
|
6
|
+
const MINUTE_FLAG_OPTS = { defaultUnit: "m", allowZero: true };
|
|
4
7
|
export function parseIterateFlags(extra, cfg) {
|
|
5
8
|
const readyDelayStr = getFlag(extra, "--ready-delay");
|
|
6
|
-
const readyDelaySuffix =
|
|
7
|
-
const readyDelaySeconds =
|
|
9
|
+
const readyDelaySuffix = validateSecondsDurationFlag("pr-shepherd", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"), MINUTE_FLAG_OPTS);
|
|
10
|
+
const readyDelaySeconds = parseDurationToSeconds(readyDelaySuffix ?? "", cfg.watch.readyDelayMinutes * 60, MINUTE_FLAG_OPTS);
|
|
8
11
|
const noAutoMarkReady = hasFlag(extra, "--no-auto-mark-ready");
|
|
9
12
|
const noAutoCancelActionable = hasFlag(extra, "--no-auto-cancel-actionable");
|
|
10
13
|
const stallTimeoutStr = getFlag(extra, "--stall-timeout");
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
: cfg.iterate.stallTimeoutMinutes * 60;
|
|
14
|
+
const stallTimeoutSuffix = validateSecondsDurationFlag("pr-shepherd", "--stall-timeout", stallTimeoutStr, hasFlag(extra, "--stall-timeout"), MINUTE_FLAG_OPTS);
|
|
15
|
+
const stallTimeoutSeconds = parseDurationToSeconds(stallTimeoutSuffix ?? "", cfg.iterate.stallTimeoutMinutes * 60, MINUTE_FLAG_OPTS);
|
|
14
16
|
return {
|
|
15
17
|
readyDelaySuffix,
|
|
16
18
|
readyDelaySeconds,
|
|
19
|
+
stallTimeoutSuffix,
|
|
17
20
|
stallTimeoutSeconds,
|
|
18
21
|
noAutoMarkReady,
|
|
19
22
|
noAutoCancelActionable,
|
|
@@ -58,7 +58,7 @@ export function formatIterateResult(result, opts) {
|
|
|
58
58
|
else if (result.mergeStatus === "CONFLICTS" && result.baseBranch) {
|
|
59
59
|
verboseBranch = ` · **branch** conflicts with \`origin/${result.baseBranch}\``;
|
|
60
60
|
}
|
|
61
|
-
summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}${verboseBranch}`;
|
|
61
|
+
summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress, ${result.summary.superseded} superseded · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}${verboseBranch}`;
|
|
62
62
|
}
|
|
63
63
|
else {
|
|
64
64
|
const counts = [`${result.summary.passing} passing`];
|
|
@@ -68,6 +68,8 @@ export function formatIterateResult(result, opts) {
|
|
|
68
68
|
counts.push(`${result.summary.filtered} filtered`);
|
|
69
69
|
if (result.summary.inProgress > 0)
|
|
70
70
|
counts.push(`${result.summary.inProgress} inProgress`);
|
|
71
|
+
if (result.summary.superseded > 0)
|
|
72
|
+
counts.push(`${result.summary.superseded} superseded`);
|
|
71
73
|
const segs = [`**summary** ${counts.join(", ")}`];
|
|
72
74
|
if (result.status === "READY" && result.remainingSeconds > 0) {
|
|
73
75
|
segs.push(`**remainingSeconds** ${result.remainingSeconds}`);
|
|
@@ -116,6 +118,10 @@ export function formatIterateResult(result, opts) {
|
|
|
116
118
|
const names = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
|
|
117
119
|
headerLines.push(`**ignored** ${names}`);
|
|
118
120
|
}
|
|
121
|
+
if (result.supersededNames && result.supersededNames.length > 0) {
|
|
122
|
+
const names = result.supersededNames.map((n) => "`" + n + "`").join(", ");
|
|
123
|
+
headerLines.push(`**superseded** ${names}`);
|
|
124
|
+
}
|
|
119
125
|
const activityLine = formatActivityLine(result);
|
|
120
126
|
if (activityLine)
|
|
121
127
|
headerLines.push(activityLine);
|
|
@@ -141,6 +147,10 @@ export function formatIterateResult(result, opts) {
|
|
|
141
147
|
const ignoredStr = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
|
|
142
148
|
cancelHeaderLines.push(`**ignored** ${ignoredStr}`);
|
|
143
149
|
}
|
|
150
|
+
if (result.supersededNames && result.supersededNames.length > 0) {
|
|
151
|
+
const supersededStr = result.supersededNames.map((n) => "`" + n + "`").join(", ");
|
|
152
|
+
cancelHeaderLines.push(`**superseded** ${supersededStr}`);
|
|
153
|
+
}
|
|
144
154
|
if (activityLine)
|
|
145
155
|
cancelHeaderLines.push(activityLine);
|
|
146
156
|
return joinSections([
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -33,6 +33,7 @@ export function projectIterateLean(result, opts) {
|
|
|
33
33
|
...(result.summary.skipped > 0 && { skipped: result.summary.skipped }),
|
|
34
34
|
...(result.summary.filtered > 0 && { filtered: result.summary.filtered }),
|
|
35
35
|
...(result.summary.inProgress > 0 && { inProgress: result.summary.inProgress }),
|
|
36
|
+
...(result.summary.superseded > 0 && { superseded: result.summary.superseded }),
|
|
36
37
|
},
|
|
37
38
|
...(result.status === "READY" &&
|
|
38
39
|
result.remainingSeconds > 0 && {
|
|
@@ -58,6 +59,9 @@ export function projectIterateLean(result, opts) {
|
|
|
58
59
|
...((result.ignoredNames?.length ?? 0) > 0 && {
|
|
59
60
|
ignoredNames: result.ignoredNames,
|
|
60
61
|
}),
|
|
62
|
+
...((result.supersededNames?.length ?? 0) > 0 && {
|
|
63
|
+
supersededNames: result.supersededNames,
|
|
64
|
+
}),
|
|
61
65
|
};
|
|
62
66
|
switch (result.action) {
|
|
63
67
|
case "wait":
|
package/bin/cli/poll-handler.mjs
CHANGED
|
@@ -11,7 +11,7 @@ export async function handlePoll(args) {
|
|
|
11
11
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
12
12
|
const cfg = loadConfig();
|
|
13
13
|
const flags = parseIterateFlags(extra, cfg);
|
|
14
|
-
if (flags.readyDelaySuffix === null)
|
|
14
|
+
if (flags.readyDelaySuffix === null || flags.stallTimeoutSuffix === null)
|
|
15
15
|
return;
|
|
16
16
|
const intervalStr = getFlag(extra, "--interval");
|
|
17
17
|
const intervalSuffix = validateSecondsDurationFlag("pr-shepherd poll", "--interval", intervalStr, hasFlag(extra, "--interval"));
|
package/bin/commands/check.mjs
CHANGED
|
@@ -127,6 +127,7 @@ export async function runCheck(opts) {
|
|
|
127
127
|
filteredNames: verdict.filteredNames,
|
|
128
128
|
blockedByFilteredCheck,
|
|
129
129
|
...(verdict.ignoredNames.length > 0 && { ignoredNames: verdict.ignoredNames }),
|
|
130
|
+
...(verdict.supersededNames.length > 0 && { supersededNames: verdict.supersededNames }),
|
|
130
131
|
},
|
|
131
132
|
threads: {
|
|
132
133
|
actionable: threadVisibility.activeThreads,
|
|
@@ -21,7 +21,7 @@ export function buildFailingCheckInstructions(checks) {
|
|
|
21
21
|
parts.push("read any included log excerpt first; fetch the full log with `gh run view <runId> --log-failed` when the excerpt is insufficient; decide whether to rerun with `gh run rerun <runId> --failed` for transient infrastructure failures (network timeout, OOM kill, runner crash), or apply a code fix for real test/build failures; if GitHub omits workflow-evaluation details from API/log output, open the run URL in the GitHub UI");
|
|
22
22
|
}
|
|
23
23
|
if (hasCancelled) {
|
|
24
|
-
parts.push("for `[conclusion: CANCELLED]` entries: rerun with `gh run rerun <runId>`
|
|
24
|
+
parts.push("for `[conclusion: CANCELLED]` entries: these are not concurrency-superseded (superseded CANCELLED checks are excluded from this section and reported under `**superseded**` instead) — rerun with `gh run rerun <runId>` unless you are already pushing new commits this tick for other reasons, in which case the fresh run naturally supersedes it; do not silently treat a required CANCELLED check as resolved — do NOT confuse with IDs under `## Cancelled runs`");
|
|
25
25
|
}
|
|
26
26
|
if (hasStartupFailure) {
|
|
27
27
|
parts.push("for `[conclusion: STARTUP_FAILURE]` entries: inspect with `gh run view <runId>` and rerun with `gh run rerun <runId>` if the workflow should be retried");
|
|
@@ -25,6 +25,19 @@ export function checkEscalateTriggers(actionableThreads, threadAttempts) {
|
|
|
25
25
|
* shell commands by `buildFixInstructions`, so we reject anything outside
|
|
26
26
|
* `[A-Za-z0-9._/-]` to prevent shell injection.
|
|
27
27
|
*/
|
|
28
|
+
/**
|
|
29
|
+
* Render a seconds count as an approximate human duration: "8 seconds" below one minute,
|
|
30
|
+
* otherwise whole minutes ("166 minutes", "60 minutes") — matching the precision the
|
|
31
|
+
* generic stall timer has always reported, just adding a seconds tier for sub-minute ages
|
|
32
|
+
* instead of always flooring to "0 minutes".
|
|
33
|
+
*/
|
|
34
|
+
export function formatDurationApprox(seconds) {
|
|
35
|
+
const s = Math.max(0, Math.floor(seconds));
|
|
36
|
+
if (s < 60)
|
|
37
|
+
return `${s} second${s === 1 ? "" : "s"}`;
|
|
38
|
+
const mins = Math.floor(s / 60);
|
|
39
|
+
return `${mins} minute${mins === 1 ? "" : "s"}`;
|
|
40
|
+
}
|
|
28
41
|
export function validateBaseBranch(raw) {
|
|
29
42
|
const trimmed = raw.trim();
|
|
30
43
|
if (trimmed === "") {
|
|
@@ -64,8 +77,7 @@ export function buildEscalateHumanMessage(escalate, pr) {
|
|
|
64
77
|
: c.detailsUrl
|
|
65
78
|
? `external \`${c.detailsUrl}\``
|
|
66
79
|
: "no run ID";
|
|
67
|
-
|
|
68
|
-
lines.push(`- check \`${c.name}\` — ${c.status} ${c.source}, ${target}, waiting ${ageMinutes} minute${ageMinutes === 1 ? "" : "s"}`);
|
|
80
|
+
lines.push(`- check \`${c.name}\` — ${c.status} ${c.source}, ${target}, waiting ${formatDurationApprox(c.ageSeconds)}`);
|
|
69
81
|
if (c.summary)
|
|
70
82
|
lines.push(` > ${c.summary}`);
|
|
71
83
|
}
|
|
@@ -110,8 +122,8 @@ export function buildEscalateHumanMessage(escalate, pr) {
|
|
|
110
122
|
}
|
|
111
123
|
export function buildEscalateSuggestion(triggers, detail) {
|
|
112
124
|
if (triggers.includes("stall-timeout")) {
|
|
113
|
-
const
|
|
114
|
-
return `No progress detected for ${
|
|
125
|
+
const duration = detail ?? "60 minutes";
|
|
126
|
+
return `No progress detected for ${duration} — state has not changed. This is a manual checkpoint: inspect the PR and apply a manual fix before resuming.`;
|
|
115
127
|
}
|
|
116
128
|
if (triggers.includes("base-branch-unknown")) {
|
|
117
129
|
const reason = detail ? ` (${detail})` : "";
|
|
@@ -9,6 +9,43 @@ export function buildSummary(report) {
|
|
|
9
9
|
skipped: report.checks.skipped.length,
|
|
10
10
|
filtered: report.checks.filtered.length,
|
|
11
11
|
inProgress: report.checks.inProgress.length,
|
|
12
|
+
superseded: report.checks.supersededNames?.length ?? 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Non-blocking check-name lists (ignored/superseded), omitted from the result when empty. */
|
|
16
|
+
export function buildSuppressedCheckFields(report) {
|
|
17
|
+
return {
|
|
18
|
+
...(report.checks.ignoredNames?.length ? { ignoredNames: report.checks.ignoredNames } : {}),
|
|
19
|
+
...(report.checks.supersededNames?.length
|
|
20
|
+
? { supersededNames: report.checks.supersededNames }
|
|
21
|
+
: {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Build the `cancel` result for a merged/closed PR — caller has already updated ready-delay/stall state. */
|
|
25
|
+
export function buildTerminalCancelResult(report) {
|
|
26
|
+
const state = report.mergeStatus.state;
|
|
27
|
+
return {
|
|
28
|
+
pr: report.pr,
|
|
29
|
+
repo: report.repo,
|
|
30
|
+
status: report.status,
|
|
31
|
+
mergeStateStatus: report.mergeStatus.mergeStateStatus,
|
|
32
|
+
mergeStatus: report.mergeStatus.status,
|
|
33
|
+
reviewDecision: report.mergeStatus.reviewDecision,
|
|
34
|
+
blockingBotReviewInProgress: report.mergeStatus.blockingBotReviewInProgress,
|
|
35
|
+
isDraft: report.mergeStatus.isDraft,
|
|
36
|
+
shouldCancel: true,
|
|
37
|
+
remainingSeconds: 0,
|
|
38
|
+
state,
|
|
39
|
+
summary: buildSummary(report),
|
|
40
|
+
baseBranch: report.baseBranch,
|
|
41
|
+
branchProtection: report.branchProtection,
|
|
42
|
+
checks: buildRelevantChecks(report),
|
|
43
|
+
inProgressChecks: buildActiveChecks(report),
|
|
44
|
+
...buildSuppressedCheckFields(report),
|
|
45
|
+
activity: report.activity,
|
|
46
|
+
action: "cancel",
|
|
47
|
+
reason: state === "MERGED" ? "merged" : "closed",
|
|
48
|
+
log: `CANCEL: PR #${report.pr} is ${state.toLowerCase()} — stopping`,
|
|
12
49
|
};
|
|
13
50
|
}
|
|
14
51
|
/** Build completed, non-skipped checks relevant to PR readiness. */
|
|
@@ -4,7 +4,7 @@ import { getCurrentPrNumber } from "../../github/client.mjs";
|
|
|
4
4
|
import { graphql } from "../../github/http.mjs";
|
|
5
5
|
import { MARK_PR_READY_MUTATION } from "../../github/queries.mjs";
|
|
6
6
|
import { loadConfig } from "../../config/load.mjs";
|
|
7
|
-
import { getCurrentHeadSha, buildSummary, buildRelevantChecks, buildActiveChecks, buildWaitLog, } from "./helpers.mjs";
|
|
7
|
+
import { getCurrentHeadSha, buildSummary, buildRelevantChecks, buildActiveChecks, buildWaitLog, buildSuppressedCheckFields, buildTerminalCancelResult, } from "./helpers.mjs";
|
|
8
8
|
import { classifyReviewSummaries } from "./classify.mjs";
|
|
9
9
|
import { applyStallGuard } from "./stall.mjs";
|
|
10
10
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
@@ -31,32 +31,9 @@ export async function runIterate(opts) {
|
|
|
31
31
|
}
|
|
32
32
|
const stallKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
33
33
|
if (report.mergeStatus.state !== "OPEN") {
|
|
34
|
-
const state = report.mergeStatus.state.toLowerCase();
|
|
35
34
|
await updateReadyDelay(report.pr, false, readyDelaySeconds, repoOwner, repoName);
|
|
36
35
|
await clearStallState(stallKey);
|
|
37
|
-
return
|
|
38
|
-
pr: report.pr,
|
|
39
|
-
repo: report.repo,
|
|
40
|
-
status: report.status,
|
|
41
|
-
mergeStateStatus: report.mergeStatus.mergeStateStatus,
|
|
42
|
-
mergeStatus: report.mergeStatus.status,
|
|
43
|
-
reviewDecision: report.mergeStatus.reviewDecision,
|
|
44
|
-
blockingBotReviewInProgress: report.mergeStatus.blockingBotReviewInProgress,
|
|
45
|
-
isDraft: report.mergeStatus.isDraft,
|
|
46
|
-
shouldCancel: true,
|
|
47
|
-
remainingSeconds: 0,
|
|
48
|
-
state: report.mergeStatus.state,
|
|
49
|
-
summary: buildSummary(report),
|
|
50
|
-
baseBranch: report.baseBranch,
|
|
51
|
-
branchProtection: report.branchProtection,
|
|
52
|
-
checks: buildRelevantChecks(report),
|
|
53
|
-
inProgressChecks: buildActiveChecks(report),
|
|
54
|
-
...(report.checks.ignoredNames?.length ? { ignoredNames: report.checks.ignoredNames } : {}),
|
|
55
|
-
activity: report.activity,
|
|
56
|
-
action: "cancel",
|
|
57
|
-
reason: report.mergeStatus.state === "MERGED" ? "merged" : "closed",
|
|
58
|
-
log: `CANCEL: PR #${report.pr} is ${state} — stopping`,
|
|
59
|
-
};
|
|
36
|
+
return buildTerminalCancelResult(report);
|
|
60
37
|
}
|
|
61
38
|
const { minimizeIds: reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = classifyReviewSummaries({
|
|
62
39
|
firstLook: report.firstLookSummaries,
|
|
@@ -95,7 +72,7 @@ export async function runIterate(opts) {
|
|
|
95
72
|
branchProtection: report.branchProtection,
|
|
96
73
|
checks: buildRelevantChecks(report),
|
|
97
74
|
inProgressChecks: buildActiveChecks(report),
|
|
98
|
-
...(report
|
|
75
|
+
...buildSuppressedCheckFields(report),
|
|
99
76
|
activity: report.activity,
|
|
100
77
|
};
|
|
101
78
|
if (readyState.shouldCancel) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readStallState, writeStallState } from "../../state/iterate-stall.mjs";
|
|
2
2
|
import { toAgentThread, toAgentComment, toAgentStalledCheck } from "../../reporters/agent.mjs";
|
|
3
|
-
import { buildEscalateSuggestion, buildEscalateHumanMessage } from "./escalate.mjs";
|
|
3
|
+
import { buildEscalateSuggestion, buildEscalateHumanMessage, formatDurationApprox, } from "./escalate.mjs";
|
|
4
4
|
function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds) {
|
|
5
5
|
const checks = [
|
|
6
6
|
...report.checks.failing.map((f) => `failing:${f.name}:${f.conclusion}`),
|
|
@@ -37,14 +37,14 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
37
37
|
action: prospectiveResult.action,
|
|
38
38
|
});
|
|
39
39
|
if (stalledChecks.length > 0) {
|
|
40
|
-
const
|
|
40
|
+
const stalledDuration = formatDurationApprox(Math.max(...stalledChecks.map((c) => c.ageSeconds)));
|
|
41
41
|
const escalateBase = {
|
|
42
42
|
triggers: ["stall-timeout"],
|
|
43
43
|
unresolvedThreads: [],
|
|
44
44
|
ambiguousComments: [],
|
|
45
45
|
changesRequestedReviews: [],
|
|
46
46
|
stalledChecks,
|
|
47
|
-
suggestion: buildEscalateSuggestion(["stall-timeout"],
|
|
47
|
+
suggestion: buildEscalateSuggestion(["stall-timeout"], stalledDuration),
|
|
48
48
|
};
|
|
49
49
|
return {
|
|
50
50
|
...base,
|
|
@@ -68,13 +68,13 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
68
68
|
await writeStallState(stallKey, { fingerprint, firstSeenAt: nowSeconds });
|
|
69
69
|
}
|
|
70
70
|
else if (ageSeconds >= stallTimeoutSeconds) {
|
|
71
|
-
const
|
|
71
|
+
const stalledDuration = formatDurationApprox(ageSeconds);
|
|
72
72
|
const escalateBase = {
|
|
73
73
|
triggers: ["stall-timeout"],
|
|
74
74
|
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
75
75
|
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
76
76
|
changesRequestedReviews: report.changesRequestedReviews,
|
|
77
|
-
suggestion: buildEscalateSuggestion(["stall-timeout"],
|
|
77
|
+
suggestion: buildEscalateSuggestion(["stall-timeout"], stalledDuration),
|
|
78
78
|
};
|
|
79
79
|
return {
|
|
80
80
|
...base,
|
|
@@ -6,13 +6,48 @@ export function parseCreatedAt(iso) {
|
|
|
6
6
|
const ms = new Date(iso).getTime();
|
|
7
7
|
return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
function extractRunId(url) {
|
|
10
10
|
if (!url)
|
|
11
11
|
return null;
|
|
12
12
|
const m = /\/runs\/(\d+)/.exec(url);
|
|
13
13
|
return m ? (m[1] ?? null) : null;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/** Stringify a GraphQL Workflow.databaseId, when present, for use as a check-grouping key. */
|
|
16
|
+
function resolveWorkflowId(databaseId) {
|
|
17
|
+
return databaseId !== null && databaseId !== undefined ? String(databaseId) : undefined;
|
|
18
|
+
}
|
|
19
|
+
/** Map a GraphQL CheckRun context node to a CheckRun. */
|
|
20
|
+
export function mapCheckRunNode(node) {
|
|
21
|
+
const event = node.checkSuite?.workflowRun?.event ?? null;
|
|
22
|
+
const workflowName = node.checkSuite?.workflowRun?.workflow?.name?.trim() || undefined;
|
|
23
|
+
const workflowId = resolveWorkflowId(node.checkSuite?.workflowRun?.workflow?.databaseId);
|
|
24
|
+
const runId = extractRunId(node.detailsUrl);
|
|
25
|
+
const summary = extractCheckRunSummary(node.title, node.summary);
|
|
26
|
+
const rawCreatedAt = node.checkSuite?.workflowRun?.createdAt ?? node.checkSuite?.createdAt;
|
|
27
|
+
const rawUpdatedAt = node.checkSuite?.workflowRun?.updatedAt ?? node.checkSuite?.updatedAt;
|
|
28
|
+
const createdAtUnix = rawCreatedAt ? parseCreatedAt(rawCreatedAt) : undefined;
|
|
29
|
+
const startedAtUnix = node.startedAt ? parseCreatedAt(node.startedAt) : undefined;
|
|
30
|
+
const completedAtUnix = node.completedAt ? parseCreatedAt(node.completedAt) : undefined;
|
|
31
|
+
const updatedAtUnix = rawUpdatedAt ? parseCreatedAt(rawUpdatedAt) : undefined;
|
|
32
|
+
return {
|
|
33
|
+
id: node.id,
|
|
34
|
+
name: node.name,
|
|
35
|
+
status: node.status,
|
|
36
|
+
conclusion: node.conclusion,
|
|
37
|
+
source: "check_run",
|
|
38
|
+
detailsUrl: node.detailsUrl ?? "",
|
|
39
|
+
event,
|
|
40
|
+
runId,
|
|
41
|
+
...(workflowName !== undefined && { workflowName }),
|
|
42
|
+
...(workflowId !== undefined && { workflowId }),
|
|
43
|
+
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
44
|
+
...(startedAtUnix !== undefined && { startedAtUnix }),
|
|
45
|
+
...(completedAtUnix !== undefined && { completedAtUnix }),
|
|
46
|
+
...(updatedAtUnix !== undefined && { updatedAtUnix }),
|
|
47
|
+
...(summary !== undefined && { summary }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function extractCheckRunSummary(title, summary) {
|
|
16
51
|
const t = title?.trim();
|
|
17
52
|
if (t)
|
|
18
53
|
return t;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapAuthorType, parseCreatedAt,
|
|
1
|
+
import { mapAuthorType, parseCreatedAt, mapStatusContextState, latestApprovedLogins, isReviewStale, mapCheckRunNode, } from "./batch-parser-helpers.mjs";
|
|
2
2
|
import { buildPrActivitySummary } from "./activity.mjs";
|
|
3
3
|
import { parseBranchProtection } from "./branch-protection.mjs";
|
|
4
4
|
function parseReviewNode(r) {
|
|
@@ -81,34 +81,7 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
81
81
|
.map((r) => parseReviewNode(r));
|
|
82
82
|
const checks = rawCheckNodes.flatMap((node) => {
|
|
83
83
|
if (node.__typename === "CheckRun") {
|
|
84
|
-
|
|
85
|
-
const workflowName = node.checkSuite?.workflowRun?.workflow?.name?.trim() || undefined;
|
|
86
|
-
const runId = extractRunId(node.detailsUrl);
|
|
87
|
-
const summary = extractCheckRunSummary(node.title, node.summary);
|
|
88
|
-
const rawCreatedAt = node.checkSuite?.workflowRun?.createdAt ?? node.checkSuite?.createdAt;
|
|
89
|
-
const rawUpdatedAt = node.checkSuite?.workflowRun?.updatedAt ?? node.checkSuite?.updatedAt;
|
|
90
|
-
const createdAtUnix = rawCreatedAt ? parseCreatedAt(rawCreatedAt) : undefined;
|
|
91
|
-
const startedAtUnix = node.startedAt ? parseCreatedAt(node.startedAt) : undefined;
|
|
92
|
-
const completedAtUnix = node.completedAt ? parseCreatedAt(node.completedAt) : undefined;
|
|
93
|
-
const updatedAtUnix = rawUpdatedAt ? parseCreatedAt(rawUpdatedAt) : undefined;
|
|
94
|
-
return [
|
|
95
|
-
{
|
|
96
|
-
id: node.id,
|
|
97
|
-
name: node.name,
|
|
98
|
-
status: node.status,
|
|
99
|
-
conclusion: node.conclusion,
|
|
100
|
-
source: "check_run",
|
|
101
|
-
detailsUrl: node.detailsUrl ?? "",
|
|
102
|
-
event,
|
|
103
|
-
runId,
|
|
104
|
-
...(workflowName !== undefined && { workflowName }),
|
|
105
|
-
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
106
|
-
...(startedAtUnix !== undefined && { startedAtUnix }),
|
|
107
|
-
...(completedAtUnix !== undefined && { completedAtUnix }),
|
|
108
|
-
...(updatedAtUnix !== undefined && { updatedAtUnix }),
|
|
109
|
-
...(summary !== undefined && { summary }),
|
|
110
|
-
},
|
|
111
|
-
];
|
|
84
|
+
return [mapCheckRunNode(node)];
|
|
112
85
|
}
|
|
113
86
|
if (node.__typename === "StatusContext") {
|
|
114
87
|
const { status, conclusion } = mapStatusContextState(node.state);
|
package/bin/types/github.mjs
CHANGED
package/bin/types.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared type definitions for the shepherd CLI. */
|
|
2
2
|
export * from "./types/github.mjs";
|
|
3
|
+
export * from "./types/check-classification.mjs";
|
|
3
4
|
export * from "./types/activity.mjs";
|
|
4
5
|
export * from "./types/review-thread.mjs";
|
|
5
6
|
export * from "./types/agent-thread.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.3",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@vitest/coverage-v8": "^4.1.4",
|
|
39
39
|
"husky": "^9.1.7",
|
|
40
40
|
"knip": "^6.14.1",
|
|
41
|
-
"oxfmt": "^0.
|
|
41
|
+
"oxfmt": "^0.57.0",
|
|
42
42
|
"oxlint": "^1.60.0",
|
|
43
43
|
"typescript": "^6.0.3",
|
|
44
44
|
"vitest": "^4.1.4"
|