pr-shepherd 0.32.1 → 0.32.2
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/iterate-formatter.mjs +11 -1
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/commands/check.mjs +1 -0
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/helpers.mjs +37 -0
- package/bin/commands/iterate/index.mjs +3 -26
- 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 +1 -1
- 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
|
+
}
|
|
@@ -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/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");
|
|
@@ -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) {
|
|
@@ -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