pr-shepherd 0.44.1 → 0.45.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/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -4
- package/bin/checks/conclusions.d.mts +24 -1
- package/bin/checks/conclusions.mjs +39 -0
- package/bin/checks/triage.d.mts +3 -2
- package/bin/checks/triage.mjs +44 -15
- package/bin/cli/api-usage-formatter.d.mts +3 -0
- package/bin/cli/api-usage-formatter.mjs +36 -0
- package/bin/cli/error-format.d.mts +13 -0
- package/bin/cli/error-format.mjs +41 -0
- package/bin/cli/fix-formatter.mjs +12 -1
- package/bin/cli/iterate-activity-formatter.d.mts +2 -0
- package/bin/cli/iterate-activity-formatter.mjs +26 -0
- package/bin/cli/iterate-formatter.mjs +11 -28
- package/bin/cli/iterate-instructions.mjs +14 -1
- package/bin/cli/iterate-lean.mjs +1 -0
- package/bin/cli/mutate-formatter.mjs +2 -0
- package/bin/commands/check-annotations.d.mts +2 -1
- package/bin/commands/check-annotations.mjs +7 -7
- package/bin/commands/check.mjs +6 -4
- package/bin/commands/iterate/api-usage.d.mts +2 -0
- package/bin/commands/iterate/api-usage.mjs +42 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -2
- package/bin/commands/iterate/check-instructions.mjs +27 -8
- package/bin/commands/iterate/fix-code.mjs +42 -8
- package/bin/commands/iterate/index.mjs +6 -3
- package/bin/commands/iterate/merge-state.mjs +5 -2
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +9 -5
- package/bin/commands/iterate/run.d.mts +2 -0
- package/bin/commands/iterate/run.mjs +8 -0
- package/bin/commands/mark-files-as-viewed.mjs +8 -0
- package/bin/commands/poll-run.d.mts +2 -0
- package/bin/commands/poll-run.mjs +8 -0
- package/bin/commands/poll.d.mts +1 -2
- package/bin/commands/poll.mjs +29 -10
- package/bin/comments/rate-limit.d.mts +4 -0
- package/bin/comments/rate-limit.mjs +6 -0
- package/bin/config/load.d.mts +6 -1
- package/bin/config/load.mjs +33 -1
- package/bin/config.json +6 -1
- package/bin/github/api-telemetry-aggregate.d.mts +32 -0
- package/bin/github/api-telemetry-aggregate.mjs +84 -0
- package/bin/github/api-telemetry.d.mts +13 -0
- package/bin/github/api-telemetry.mjs +128 -0
- package/bin/github/check-annotations.d.mts +14 -1
- package/bin/github/check-annotations.mjs +29 -2
- package/bin/github/errors.d.mts +2 -0
- package/bin/github/errors.mjs +2 -0
- package/bin/github/gql/batch-pr-page.gql +8 -0
- package/bin/github/gql/batch-pr.gql +8 -0
- package/bin/github/gql/check-run-annotations.gql +8 -0
- package/bin/github/gql/commit-check-contexts.gql +8 -0
- package/bin/github/gql/get-pr-body.gql +8 -0
- package/bin/github/gql/get-pr-head-sha.gql +8 -0
- package/bin/github/gql/pr-number-by-branch.gql +8 -0
- package/bin/github/gql/review-thread-comments.gql +8 -0
- package/bin/github/gql/suggestion-threads.gql +8 -0
- package/bin/github/graphql-http.mjs +63 -8
- package/bin/github/http-auth.d.mts +9 -1
- package/bin/github/http-auth.mjs +35 -14
- package/bin/github/http-intermediate.d.mts +9 -0
- package/bin/github/http-intermediate.mjs +20 -0
- package/bin/github/http-request.d.mts +1 -1
- package/bin/github/http-request.mjs +1 -1
- package/bin/github/http-utils.d.mts +6 -0
- package/bin/github/http-utils.mjs +10 -1
- package/bin/github/rest-http.d.mts +16 -1
- package/bin/github/rest-http.mjs +71 -8
- package/bin/github/rest-text.mjs +41 -5
- package/bin/index.mjs +2 -1
- package/bin/log/session.d.mts +11 -0
- package/bin/log/session.mjs +22 -0
- package/bin/mcp/server.mjs +16 -2
- package/bin/quota-warning.d.mts +2 -0
- package/bin/quota-warning.mjs +6 -0
- package/bin/state/graphql-quota-claims.d.mts +2 -0
- package/bin/state/graphql-quota-claims.mjs +61 -0
- package/bin/state/graphql-quota-policy.d.mts +17 -0
- package/bin/state/graphql-quota-policy.mjs +43 -0
- package/bin/state/graphql-quota-warnings.d.mts +6 -0
- package/bin/state/graphql-quota-warnings.mjs +105 -0
- package/bin/state/rest-cache.d.mts +48 -0
- package/bin/state/rest-cache.mjs +91 -0
- package/bin/types/api-usage.d.mts +31 -0
- package/bin/types/api-usage.mjs +1 -0
- package/bin/types/iterate.d.mts +4 -1
- package/bin/types/report.d.mts +2 -0
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +14 -9
package/README.md
CHANGED
|
@@ -69,12 +69,12 @@ Conversations Resolved: No [Not Required]
|
|
|
69
69
|
1. Review each item under `## Review threads` and `## Failing checks` and decide whether it needs a code change.
|
|
70
70
|
2. Apply every warranted review fix in each file referenced above.
|
|
71
71
|
3. Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.
|
|
72
|
-
4. If you changed code, commit any remaining changes
|
|
72
|
+
4. If you changed code, commit any remaining changes and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate again with the same options. If you did not change code, do not commit and continue.
|
|
73
73
|
5. Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an earlier Shepherd reply: a marked viewer-authored human thread is emitted resolve-only when authorized, while a marked other-human thread is already acknowledged and has no further mutation.
|
|
74
|
-
6. If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head.
|
|
74
|
+
6. If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.
|
|
75
75
|
7. Replace `$DISMISS_MESSAGE` with one sentence describing what changed.
|
|
76
76
|
8. Run the `apply review:` command shown above. See "Review-mutation mechanics" in the pr-shepherd skill for dismiss-ID retention.
|
|
77
|
-
9. `[FIX_CODE]` is
|
|
77
|
+
9. `[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate again with the same options; without code changes, complete the authorized review mutations and iterate again.
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
See [docs/actions.md](docs/actions.md) for the complete output contract. Iterate/poll PR outcomes use exit codes `0` and `10`–`15`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
|
|
@@ -88,7 +88,7 @@ This system is opinionated and works best with PRs that use required status chec
|
|
|
88
88
|
- Shepherd identifies its own latest reply only when that comment begins `<!-- pr-shepherd -->`, not from author equality. A marked viewer-authored thread can be resolved without another reply as a retry.
|
|
89
89
|
- Every review thread/comment/review summary is surfaced at least once, even if already outdated, resolved, or minimized; edited items re-surface through seen markers.
|
|
90
90
|
- Draft PRs can be marked ready automatically when clean; disable with `actions.autoMarkReady: false` or `--no-auto-mark-ready`.
|
|
91
|
-
- The CLI never performs git mutations. It
|
|
91
|
+
- The CLI never performs git mutations itself — it only emits commit/push instructions for the agent to run. It recommends pushing autonomously (own-repo PRs, and fork PRs where the viewer has push access to the fork) unless GitHub's viewer fields affirmatively report no head-branch push access, in which case it hands the push off to a human instead.
|
|
92
92
|
- Every GitHub mutation is permission-aware. Shepherd uses raw viewer capability fields, omits unauthorized commands, and repeats authorization checks in direct `apply` commands. Missing capability data fails closed.
|
|
93
93
|
- `build_suggestion_patches` turns one or more ordered GitHub suggestion threads into checked patches and commit metadata, but never edits the working tree or git history. Local HEAD may be ahead when the live PR head is its ancestor.
|
|
94
94
|
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
import type { CheckConclusion } from "../types.mts";
|
|
1
|
+
import type { CheckConclusion, ViewerAuthorization } from "../types.mts";
|
|
2
|
+
/**
|
|
3
|
+
* True when the viewer's base-repo role grants GitHub's Actions rerun capability
|
|
4
|
+
* (`actions: write`, which rides with push access). The rerun runs against the base
|
|
5
|
+
* repo where the run lives, so `repositoryPermission` — not `headRepositoryPermission`,
|
|
6
|
+
* which covers fork push access — is the right field to gate on.
|
|
7
|
+
*/
|
|
8
|
+
export declare function canRerunWorkflows(auth: ViewerAuthorization | undefined): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* True when the viewer can push commits to the PR head branch — the fork, for a fork PR
|
|
11
|
+
* (own-fork PRs report `headRepositoryPermission: "ADMIN"`), or the base repo otherwise.
|
|
12
|
+
* `viewerCanEditFiles` is GitHub's direct "can the viewer commit to this PR" signal (true
|
|
13
|
+
* for fork authors and for maintainers with "allow edits from maintainers" enabled) and
|
|
14
|
+
* takes priority when present.
|
|
15
|
+
*
|
|
16
|
+
* pr-shepherd's premise is that the caller has push access to the PR it's iterating, so
|
|
17
|
+
* this defaults to pushable: it withholds the autonomous push only when GitHub
|
|
18
|
+
* affirmatively reports no head-branch access (`viewerCanEditFiles === false` and
|
|
19
|
+
* `headRepositoryPermission` in `NONE`/`READ`/`TRIAGE`). An unverifiable/unknown signal —
|
|
20
|
+
* matching the residual risk already accepted for CI reruns (see `canRerunWorkflows`) —
|
|
21
|
+
* is treated as pushable rather than pre-emptively handed off; a push without access
|
|
22
|
+
* simply fails when attempted.
|
|
23
|
+
*/
|
|
24
|
+
export declare function canPushToHead(auth: ViewerAuthorization | undefined): boolean;
|
|
2
25
|
/** Failing-check rows for formatter/instructions — excludes annotation-only carriers. */
|
|
3
26
|
export declare function isFailingAgentCheck(check: {
|
|
4
27
|
conclusion: CheckConclusion;
|
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
const NON_FAILING_CONCLUSIONS = new Set(["SUCCESS", "SKIPPED", "NEUTRAL"]);
|
|
2
|
+
/** Repository roles GitHub grants `actions: write` to — the exact capability a workflow-run rerun needs. */
|
|
3
|
+
const RERUN_CAPABLE_PERMISSIONS = new Set(["WRITE", "MAINTAIN", "ADMIN"]);
|
|
4
|
+
/**
|
|
5
|
+
* True when the viewer's base-repo role grants GitHub's Actions rerun capability
|
|
6
|
+
* (`actions: write`, which rides with push access). The rerun runs against the base
|
|
7
|
+
* repo where the run lives, so `repositoryPermission` — not `headRepositoryPermission`,
|
|
8
|
+
* which covers fork push access — is the right field to gate on.
|
|
9
|
+
*/
|
|
10
|
+
export function canRerunWorkflows(auth) {
|
|
11
|
+
const permission = auth?.repositoryPermission;
|
|
12
|
+
return permission != null && RERUN_CAPABLE_PERMISSIONS.has(permission);
|
|
13
|
+
}
|
|
14
|
+
/** Repository roles that carry push access to the head branch. */
|
|
15
|
+
const PUSH_CAPABLE_PERMISSIONS = new Set(["WRITE", "MAINTAIN", "ADMIN"]);
|
|
16
|
+
/**
|
|
17
|
+
* True when the viewer can push commits to the PR head branch — the fork, for a fork PR
|
|
18
|
+
* (own-fork PRs report `headRepositoryPermission: "ADMIN"`), or the base repo otherwise.
|
|
19
|
+
* `viewerCanEditFiles` is GitHub's direct "can the viewer commit to this PR" signal (true
|
|
20
|
+
* for fork authors and for maintainers with "allow edits from maintainers" enabled) and
|
|
21
|
+
* takes priority when present.
|
|
22
|
+
*
|
|
23
|
+
* pr-shepherd's premise is that the caller has push access to the PR it's iterating, so
|
|
24
|
+
* this defaults to pushable: it withholds the autonomous push only when GitHub
|
|
25
|
+
* affirmatively reports no head-branch access (`viewerCanEditFiles === false` and
|
|
26
|
+
* `headRepositoryPermission` in `NONE`/`READ`/`TRIAGE`). An unverifiable/unknown signal —
|
|
27
|
+
* matching the residual risk already accepted for CI reruns (see `canRerunWorkflows`) —
|
|
28
|
+
* is treated as pushable rather than pre-emptively handed off; a push without access
|
|
29
|
+
* simply fails when attempted.
|
|
30
|
+
*/
|
|
31
|
+
export function canPushToHead(auth) {
|
|
32
|
+
if (auth === undefined)
|
|
33
|
+
return true;
|
|
34
|
+
if (auth.viewerCanEditFiles === true)
|
|
35
|
+
return true;
|
|
36
|
+
const permission = auth.headRepositoryPermission;
|
|
37
|
+
if (permission != null && !PUSH_CAPABLE_PERMISSIONS.has(permission))
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
2
41
|
/** True for conclusions that belong under `## Failing checks` (not success/skipped/neutral). */
|
|
3
42
|
function isFailingCheckConclusion(conclusion) {
|
|
4
43
|
return conclusion == null || !NON_FAILING_CONCLUSIONS.has(conclusion);
|
package/bin/checks/triage.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CheckRun, ClassifiedCheck, TriagedCheck } from "../types.mts";
|
|
2
2
|
import type { RepoInfo } from "../github/client.mts";
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
3
|
+
import { type StateKey } from "../state/rest-cache.mts";
|
|
4
|
+
export declare function triageFailingChecks(failingChecks: ClassifiedCheck[], repo: RepoInfo, stateKey?: StateKey): Promise<TriagedCheck[]>;
|
|
5
|
+
export declare function fetchStartupFailureChecks(repo: RepoInfo, headSha: string, prNumber: number, stateKey?: StateKey): Promise<CheckRun[]>;
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { restWithRateLimit, restText } from "../github/http.mjs";
|
|
3
|
+
import { loadDerived, storeDerived } from "../state/rest-cache.mjs";
|
|
3
4
|
const STARTUP_FAILURE_STATUS = "startup_failure";
|
|
4
5
|
const LOG_EXCERPT_CONTEXT_LINES = 16;
|
|
5
6
|
const LOG_EXCERPT_TAIL_LINES = 28;
|
|
6
7
|
const LOG_EXCERPT_MAX_CHARS = 4_000;
|
|
7
8
|
const TRUNCATED_SUFFIX = "\n[truncated]";
|
|
8
9
|
const ANSI_SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
9
|
-
export function triageFailingChecks(failingChecks, repo) {
|
|
10
|
+
export function triageFailingChecks(failingChecks, repo, stateKey) {
|
|
10
11
|
const jobsCache = new Map();
|
|
11
|
-
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
|
|
12
|
+
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache, stateKey)));
|
|
12
13
|
}
|
|
13
|
-
async function triageCheck(check, repo, jobsCache) {
|
|
14
|
+
async function triageCheck(check, repo, jobsCache, stateKey) {
|
|
14
15
|
if (check.runId === null ||
|
|
15
16
|
check.conclusion === "CANCELLED" ||
|
|
16
17
|
check.conclusion === "STARTUP_FAILURE") {
|
|
17
18
|
return { ...check };
|
|
18
19
|
}
|
|
19
|
-
const jobs = await fetchJobs(check.runId, repo, jobsCache);
|
|
20
|
+
const jobs = await fetchJobs(check.runId, repo, jobsCache, stateKey);
|
|
20
21
|
const jobInfo = jobs ? pickJobInfo(jobs, check.name) : undefined;
|
|
21
|
-
const logExcerpt = jobInfo?.jobId
|
|
22
|
+
const logExcerpt = jobInfo?.jobId
|
|
23
|
+
? await fetchJobLogExcerpt(jobInfo.jobId, repo, stateKey, jobInfo.jobConclusion != null)
|
|
24
|
+
: undefined;
|
|
22
25
|
return {
|
|
23
26
|
...check,
|
|
24
27
|
...(jobInfo?.workflowName !== undefined && { workflowName: jobInfo.workflowName }),
|
|
@@ -27,9 +30,9 @@ async function triageCheck(check, repo, jobsCache) {
|
|
|
27
30
|
...(logExcerpt !== undefined && { logExcerpt }),
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
|
-
export async function fetchStartupFailureChecks(repo, headSha, prNumber) {
|
|
33
|
+
export async function fetchStartupFailureChecks(repo, headSha, prNumber, stateKey) {
|
|
31
34
|
try {
|
|
32
|
-
return await fetchStartupFailureChecksUncached(repo, headSha, prNumber);
|
|
35
|
+
return await fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey);
|
|
33
36
|
}
|
|
34
37
|
catch (err) {
|
|
35
38
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -37,13 +40,21 @@ export async function fetchStartupFailureChecks(repo, headSha, prNumber) {
|
|
|
37
40
|
return [];
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
|
-
async function fetchStartupFailureChecksUncached(repo, headSha, prNumber) {
|
|
43
|
+
async function fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey) {
|
|
41
44
|
const { owner, name } = repo;
|
|
42
45
|
const perPage = 100;
|
|
43
46
|
const MAX_RUN_PAGES = 10;
|
|
44
47
|
const checks = [];
|
|
45
48
|
for (let page = 1; page <= MAX_RUN_PAGES; page++) {
|
|
46
|
-
const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}
|
|
49
|
+
const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`, undefined, stateKey
|
|
50
|
+
? {
|
|
51
|
+
conditional: {
|
|
52
|
+
key: stateKey,
|
|
53
|
+
name: `runs-startupfailure-${headSha}-p${page}`,
|
|
54
|
+
headSha,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
: undefined);
|
|
47
58
|
checks.push(...data.workflow_runs
|
|
48
59
|
.filter((run) => runBelongsToPr(run, prNumber, headSha))
|
|
49
60
|
.map(workflowRunToCheckRun));
|
|
@@ -75,15 +86,15 @@ function workflowRunToCheckRun(run) {
|
|
|
75
86
|
function runBelongsToPr(run, prNumber, headSha) {
|
|
76
87
|
return (run.pull_requests ?? []).some((pr) => pr.number === prNumber && (pr.head?.sha ?? headSha) === headSha);
|
|
77
88
|
}
|
|
78
|
-
function fetchJobs(runId, repo, cache) {
|
|
89
|
+
function fetchJobs(runId, repo, cache, stateKey) {
|
|
79
90
|
const cached = cache.get(runId);
|
|
80
91
|
if (cached)
|
|
81
92
|
return cached;
|
|
82
|
-
const promise = fetchJobsUncached(runId, repo);
|
|
93
|
+
const promise = fetchJobsUncached(runId, repo, stateKey);
|
|
83
94
|
cache.set(runId, promise);
|
|
84
95
|
return promise;
|
|
85
96
|
}
|
|
86
|
-
async function fetchJobsUncached(runId, repo) {
|
|
97
|
+
async function fetchJobsUncached(runId, repo, stateKey) {
|
|
87
98
|
const { owner, name } = repo;
|
|
88
99
|
const perPage = 100;
|
|
89
100
|
const MAX_JOB_PAGES = 20; // 2000 jobs max
|
|
@@ -95,7 +106,9 @@ async function fetchJobsUncached(runId, repo) {
|
|
|
95
106
|
process.stderr.write(`pr-shepherd: job pagination cap (${MAX_JOB_PAGES * 100} jobs) reached for run ${runId} — triage may be incomplete\n`);
|
|
96
107
|
break;
|
|
97
108
|
}
|
|
98
|
-
const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}
|
|
109
|
+
const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}`, undefined, stateKey
|
|
110
|
+
? { conditional: { key: stateKey, name: `jobs-run-${runId}-p${page}` } }
|
|
111
|
+
: undefined);
|
|
99
112
|
allJobs.push(...data.jobs);
|
|
100
113
|
if (rateLimit?.remaining === 0) {
|
|
101
114
|
process.stderr.write(`pr-shepherd: REST rate limit remaining is 0 while listing jobs for run ${runId} — triage may be incomplete\n`);
|
|
@@ -127,12 +140,28 @@ function pickJobInfo(jobs, checkName) {
|
|
|
127
140
|
workflowName: job.workflow_name,
|
|
128
141
|
jobName: job.name,
|
|
129
142
|
failedStep,
|
|
143
|
+
jobConclusion: job.conclusion,
|
|
130
144
|
};
|
|
131
145
|
}
|
|
132
|
-
|
|
146
|
+
/**
|
|
147
|
+
* `cacheable` gates the cross-tick cache — only set once the matched job has
|
|
148
|
+
* a terminal conclusion, so an in-progress job's (possibly partial) log
|
|
149
|
+
* never gets frozen into the cache.
|
|
150
|
+
*/
|
|
151
|
+
async function fetchJobLogExcerpt(jobId, repo, stateKey, cacheable = false) {
|
|
152
|
+
const cacheName = `joblog-${jobId}`;
|
|
153
|
+
if (stateKey && cacheable) {
|
|
154
|
+
const cached = await loadDerived(stateKey, cacheName);
|
|
155
|
+
if (cached)
|
|
156
|
+
return cached.value ?? undefined;
|
|
157
|
+
}
|
|
133
158
|
const { owner, name } = repo;
|
|
134
159
|
try {
|
|
135
|
-
|
|
160
|
+
const excerpt = buildLogExcerpt(await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`));
|
|
161
|
+
if (stateKey && cacheable) {
|
|
162
|
+
await storeDerived(stateKey, cacheName, excerpt ?? null);
|
|
163
|
+
}
|
|
164
|
+
return excerpt;
|
|
136
165
|
}
|
|
137
166
|
catch {
|
|
138
167
|
return undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function resetTime(resetAt) {
|
|
2
|
+
return new Date(resetAt * 1000).toISOString();
|
|
3
|
+
}
|
|
4
|
+
function formatResource(resource) {
|
|
5
|
+
const used = resource.used === undefined ? "" : ` · used ${resource.used}`;
|
|
6
|
+
return `- \`${resource.resource}\`: ${resource.remaining}/${resource.limit} remaining${used} · ${resource.requestCount} requests · resets ${resetTime(resource.resetAt)}`;
|
|
7
|
+
}
|
|
8
|
+
export function formatQuotaWarning(warning) {
|
|
9
|
+
if (warning === undefined)
|
|
10
|
+
return null;
|
|
11
|
+
const used = warning.used === undefined ? "" : ` · used ${warning.used}`;
|
|
12
|
+
return [
|
|
13
|
+
"## GitHub API quota warning",
|
|
14
|
+
"",
|
|
15
|
+
`- Resource: \`${warning.resource}\``,
|
|
16
|
+
`- Remaining: ${warning.remaining}/${warning.limit}${used}`,
|
|
17
|
+
`- Crossed threshold: ${warning.thresholdPercent}% remaining`,
|
|
18
|
+
`- Reset: ${resetTime(warning.resetAt)}`,
|
|
19
|
+
`- Recommended poll interval: ${warning.pollIntervalMinutes} minutes`,
|
|
20
|
+
`- Recommended bounded CLI timeout: ${warning.pollTimeoutMinutes} minutes`,
|
|
21
|
+
"- Recommendation: prefer non-GraphQL `gh` CLI commands for PR operations until the reset above, then resume pr-shepherd",
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
export function formatApiUsage(usage) {
|
|
25
|
+
if (usage === undefined)
|
|
26
|
+
return null;
|
|
27
|
+
const lines = ["## GitHub API usage", ""];
|
|
28
|
+
lines.push(`- Credential source: ${usage.credentialSources.map((source) => `\`${source}\``).join(", ")}`);
|
|
29
|
+
if (usage.graphql !== undefined) {
|
|
30
|
+
lines.push(formatResource(usage.graphql));
|
|
31
|
+
lines.push(`- GraphQL measured cost: ${usage.graphql.measuredQueryCost} · unmeasured requests: ${usage.graphql.unmeasuredRequestCount} · nodes: ${usage.graphql.nodeCount}`);
|
|
32
|
+
}
|
|
33
|
+
for (const resource of usage.rest ?? [])
|
|
34
|
+
lines.push(formatResource(resource));
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Safe GitHub transport metadata suitable for user-facing structured error output. */
|
|
2
|
+
export declare function serializeGitHubRequestErrorDetails(err: unknown): {
|
|
3
|
+
rateLimit?: {
|
|
4
|
+
resource: string;
|
|
5
|
+
limit: number;
|
|
6
|
+
remaining: number;
|
|
7
|
+
used?: number | undefined;
|
|
8
|
+
resetAt: number;
|
|
9
|
+
} | undefined;
|
|
10
|
+
retryAfterSeconds?: number | undefined;
|
|
11
|
+
credential?: string | undefined;
|
|
12
|
+
} | undefined;
|
|
13
|
+
export declare function formatCliError(err: unknown): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { GitHubRequestError } from "../github/errors.mjs";
|
|
2
|
+
/** Safe GitHub transport metadata suitable for user-facing structured error output. */
|
|
3
|
+
export function serializeGitHubRequestErrorDetails(err) {
|
|
4
|
+
if (!(err instanceof GitHubRequestError))
|
|
5
|
+
return undefined;
|
|
6
|
+
return {
|
|
7
|
+
...(err.rateLimit !== undefined && {
|
|
8
|
+
rateLimit: {
|
|
9
|
+
resource: err.rateLimit.resource ?? "unknown",
|
|
10
|
+
limit: err.rateLimit.limit,
|
|
11
|
+
remaining: err.rateLimit.remaining,
|
|
12
|
+
...(err.rateLimit.used !== undefined && { used: err.rateLimit.used }),
|
|
13
|
+
resetAt: err.rateLimit.resetAt,
|
|
14
|
+
},
|
|
15
|
+
}),
|
|
16
|
+
...(err.retryAfterSeconds !== undefined && { retryAfterSeconds: err.retryAfterSeconds }),
|
|
17
|
+
...(err.authSource !== undefined && { credential: err.authSource }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function formatCliError(err) {
|
|
21
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22
|
+
const githubDetails = serializeGitHubRequestErrorDetails(err);
|
|
23
|
+
if (githubDetails === undefined)
|
|
24
|
+
return message;
|
|
25
|
+
const details = [];
|
|
26
|
+
if (githubDetails.rateLimit !== undefined) {
|
|
27
|
+
details.push(`resource ${githubDetails.rateLimit.resource}`);
|
|
28
|
+
details.push(`remaining ${githubDetails.rateLimit.remaining}/${githubDetails.rateLimit.limit}`);
|
|
29
|
+
if (githubDetails.rateLimit.used !== undefined) {
|
|
30
|
+
details.push(`used ${githubDetails.rateLimit.used}`);
|
|
31
|
+
}
|
|
32
|
+
details.push(`reset ${new Date(githubDetails.rateLimit.resetAt * 1000).toISOString()}`);
|
|
33
|
+
}
|
|
34
|
+
if (githubDetails.retryAfterSeconds !== undefined) {
|
|
35
|
+
details.push(`retry after ${githubDetails.retryAfterSeconds}s`);
|
|
36
|
+
}
|
|
37
|
+
if (githubDetails.credential !== undefined) {
|
|
38
|
+
details.push(`credential ${githubDetails.credential}`);
|
|
39
|
+
}
|
|
40
|
+
return details.length === 0 ? message : `${message} (${details.join("; ")})`;
|
|
41
|
+
}
|
|
@@ -44,6 +44,10 @@ export function formatFixCodeResult(header, result) {
|
|
|
44
44
|
const failingChecks = result.fix.checks.filter((ch) => isFailingAgentCheck(ch));
|
|
45
45
|
if (failingChecks.length > 0) {
|
|
46
46
|
sections.push("## Failing checks");
|
|
47
|
+
// Multiple failing CheckRuns (matrix jobs, etc.) can share one runId; the rerun command is
|
|
48
|
+
// identical for all of them, so print it once — after the first bullet for that runId —
|
|
49
|
+
// instead of repeating the same shell command on every row.
|
|
50
|
+
const seenRerunRunIds = new Set();
|
|
47
51
|
const bullets = failingChecks.map((ch) => {
|
|
48
52
|
const workflowPrefix = ch.workflowName ? `${ch.workflowName} › ` : "";
|
|
49
53
|
const jobLabel = ch.jobName ? ch.jobName : ch.name;
|
|
@@ -56,7 +60,10 @@ export function formatFixCodeResult(header, result) {
|
|
|
56
60
|
const scopeTag = ch.scope
|
|
57
61
|
? ` [scope: ${ch.scope}${ch.commitOid ? `, commit: ${ch.commitOid}` : ""}]`
|
|
58
62
|
: "";
|
|
59
|
-
const
|
|
63
|
+
const rerunTag = ch.rerunCommand ? " [rerun authorized]" : "";
|
|
64
|
+
const lines = [
|
|
65
|
+
`- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}${scopeTag}${rerunTag}`,
|
|
66
|
+
];
|
|
60
67
|
if (ch.conclusion !== "CANCELLED") {
|
|
61
68
|
if (ch.failedStep)
|
|
62
69
|
lines.push(` > ${ch.failedStep}`);
|
|
@@ -65,6 +72,10 @@ export function formatFixCodeResult(header, result) {
|
|
|
65
72
|
if (ch.logExcerpt)
|
|
66
73
|
lines.push(indentBlockquote(ch.logExcerpt, " "));
|
|
67
74
|
}
|
|
75
|
+
if (ch.rerunCommand && ch.runId && !seenRerunRunIds.has(ch.runId)) {
|
|
76
|
+
seenRerunRunIds.add(ch.runId);
|
|
77
|
+
lines.push(` rerun: \`${ch.rerunCommand}\``);
|
|
78
|
+
}
|
|
68
79
|
return lines.join("\n");
|
|
69
80
|
});
|
|
70
81
|
sections.push(bullets.join("\n\n"));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function formatActivityLine(result) {
|
|
2
|
+
const activity = result.activity ?? {
|
|
3
|
+
commitCount: 0,
|
|
4
|
+
reviewRoundCount: 0,
|
|
5
|
+
latestCommitCommittedAtUnix: null,
|
|
6
|
+
reviewItemsSinceLatestCommit: [],
|
|
7
|
+
};
|
|
8
|
+
const hasActiveChecks = (result.inProgressChecks?.length ?? 0) > 0;
|
|
9
|
+
if (activity.commitCount === 0 &&
|
|
10
|
+
activity.reviewRoundCount === 0 &&
|
|
11
|
+
activity.reviewItemsSinceLatestCommit.length === 0 &&
|
|
12
|
+
!hasActiveChecks) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const parts = [`${activity.commitCount} commits`, `${activity.reviewRoundCount} review rounds`];
|
|
16
|
+
if (activity.reviewItemsSinceLatestCommit.length > 0) {
|
|
17
|
+
parts.push(`${activity.reviewItemsSinceLatestCommit.length} review items since latest commit`);
|
|
18
|
+
}
|
|
19
|
+
if (hasActiveChecks) {
|
|
20
|
+
parts.push(`active: ${result
|
|
21
|
+
.inProgressChecks.slice(0, 5)
|
|
22
|
+
.map((check) => `\`${check.name}\``)
|
|
23
|
+
.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
return `**activity** ${parts.join(" · ")}`;
|
|
26
|
+
}
|
|
@@ -3,32 +3,8 @@ import { joinSections } from "../util/markdown.mjs";
|
|
|
3
3
|
import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, } from "./iterate-instructions.mjs";
|
|
4
4
|
import { formatMergeRequirementLines } from "../merge-status/requirements-format.mjs";
|
|
5
5
|
import { appendMergeQueueHeader, formatMergeAction } from "./iterate-merge-formatter.mjs";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
commitCount: 0,
|
|
9
|
-
reviewRoundCount: 0,
|
|
10
|
-
latestCommitCommittedAtUnix: null,
|
|
11
|
-
reviewItemsSinceLatestCommit: [],
|
|
12
|
-
};
|
|
13
|
-
const hasActiveChecks = (result.inProgressChecks?.length ?? 0) > 0;
|
|
14
|
-
if (activity.commitCount === 0 &&
|
|
15
|
-
activity.reviewRoundCount === 0 &&
|
|
16
|
-
activity.reviewItemsSinceLatestCommit.length === 0 &&
|
|
17
|
-
!hasActiveChecks) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const parts = [`${activity.commitCount} commits`, `${activity.reviewRoundCount} review rounds`];
|
|
21
|
-
if (activity.reviewItemsSinceLatestCommit.length > 0) {
|
|
22
|
-
parts.push(`${activity.reviewItemsSinceLatestCommit.length} review items since latest commit`);
|
|
23
|
-
}
|
|
24
|
-
if (hasActiveChecks) {
|
|
25
|
-
parts.push(`active: ${result
|
|
26
|
-
.inProgressChecks.slice(0, 5)
|
|
27
|
-
.map((c) => `\`${c.name}\``)
|
|
28
|
-
.join(", ")}`);
|
|
29
|
-
}
|
|
30
|
-
return `**activity** ${parts.join(" · ")}`;
|
|
31
|
-
}
|
|
6
|
+
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
7
|
+
import { formatActivityLine } from "./iterate-activity-formatter.mjs";
|
|
32
8
|
/**
|
|
33
9
|
* Format an IterateResult as human-readable Markdown.
|
|
34
10
|
*
|
|
@@ -132,21 +108,26 @@ export function formatIterateResult(result, opts) {
|
|
|
132
108
|
if (activityLine)
|
|
133
109
|
headerLines.push(activityLine);
|
|
134
110
|
const header = headerLines.join("\n");
|
|
111
|
+
const quotaWarning = formatQuotaWarning(result.quotaWarning);
|
|
112
|
+
const apiUsage = verbose ? formatApiUsage(result.apiUsage) : null;
|
|
113
|
+
const telemetrySections = [quotaWarning, apiUsage];
|
|
135
114
|
switch (result.action) {
|
|
136
115
|
case "wait":
|
|
137
116
|
return joinSections([
|
|
138
117
|
header,
|
|
118
|
+
...telemetrySections,
|
|
139
119
|
adaptIterateLog(result.log),
|
|
140
120
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
141
121
|
]);
|
|
142
122
|
case "mark_ready":
|
|
143
123
|
return joinSections([
|
|
144
124
|
header,
|
|
125
|
+
...telemetrySections,
|
|
145
126
|
adaptIterateLog(result.log),
|
|
146
127
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
147
128
|
]);
|
|
148
129
|
case "merge":
|
|
149
|
-
return formatMergeAction(header, result);
|
|
130
|
+
return formatMergeAction(joinSections([header, ...telemetrySections]), result);
|
|
150
131
|
case "cancel": {
|
|
151
132
|
const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
|
|
152
133
|
if (result.mergeRequirements) {
|
|
@@ -167,6 +148,7 @@ export function formatIterateResult(result, opts) {
|
|
|
167
148
|
cancelHeaderLines.push(activityLine);
|
|
168
149
|
return joinSections([
|
|
169
150
|
cancelHeaderLines.join("\n"),
|
|
151
|
+
...(apiUsage ? [apiUsage] : []),
|
|
170
152
|
adaptIterateLog(result.log),
|
|
171
153
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
172
154
|
]);
|
|
@@ -174,10 +156,11 @@ export function formatIterateResult(result, opts) {
|
|
|
174
156
|
case "escalate":
|
|
175
157
|
return joinSections([
|
|
176
158
|
header,
|
|
159
|
+
...(apiUsage ? [apiUsage] : []),
|
|
177
160
|
result.escalate.humanMessage,
|
|
178
161
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
179
162
|
]);
|
|
180
163
|
case "fix_code":
|
|
181
|
-
return formatFixCodeResult(header, result);
|
|
164
|
+
return formatFixCodeResult(joinSections([header, ...telemetrySections]), result);
|
|
182
165
|
}
|
|
183
166
|
}
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import { renderMergeCommand } from "../commands/iterate/merge.mjs";
|
|
2
2
|
import { inlineCode } from "../util/markdown.mjs";
|
|
3
|
+
import { buildQuotaAwareContinuation } from "../quota-warning.mjs";
|
|
3
4
|
export function buildSimpleIterateInstructions(result) {
|
|
4
5
|
switch (result.action) {
|
|
5
6
|
case "wait":
|
|
7
|
+
if (result.quotaWarning) {
|
|
8
|
+
return [
|
|
9
|
+
buildQuotaAwareContinuation(result.quotaWarning, "Non-terminal — no action needed this tick."),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
6
12
|
return [
|
|
7
13
|
"Non-terminal — no action needed this tick. Iterate again with the same options to continue.",
|
|
8
14
|
];
|
|
9
15
|
case "mark_ready":
|
|
16
|
+
if (result.quotaWarning) {
|
|
17
|
+
return [
|
|
18
|
+
buildQuotaAwareContinuation(result.quotaWarning, "The CLI marked the PR ready for review."),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
10
21
|
return [
|
|
11
22
|
"The CLI marked the PR ready for review. Iterate again with the same options to continue.",
|
|
12
23
|
];
|
|
@@ -20,7 +31,9 @@ export function buildSimpleIterateInstructions(result) {
|
|
|
20
31
|
else if (result.merge.fallbackCommand) {
|
|
21
32
|
instructions.push(`Only if GitHub reports that auto-merge is unavailable, run the \`plain merge fallback\` command: ${inlineCode(renderMergeCommand(result.merge.fallbackCommand))}.`);
|
|
22
33
|
}
|
|
23
|
-
instructions.push(
|
|
34
|
+
instructions.push(result.quotaWarning
|
|
35
|
+
? buildQuotaAwareContinuation(result.quotaWarning, "After running the merge command.")
|
|
36
|
+
: "Then iterate again with the same options to monitor until the PR merges or needs work.");
|
|
24
37
|
return instructions;
|
|
25
38
|
}
|
|
26
39
|
case "cancel":
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -64,6 +64,7 @@ export function projectIterateLean(result, opts) {
|
|
|
64
64
|
...((result.supersededNames?.length ?? 0) > 0 && {
|
|
65
65
|
supersededNames: result.supersededNames,
|
|
66
66
|
}),
|
|
67
|
+
...(result.quotaWarning && { quotaWarning: result.quotaWarning }),
|
|
67
68
|
};
|
|
68
69
|
switch (result.action) {
|
|
69
70
|
case "wait":
|
|
@@ -6,12 +6,14 @@ function formatRateLimit(result) {
|
|
|
6
6
|
const rateLimit = result.rateLimit;
|
|
7
7
|
if (rateLimit) {
|
|
8
8
|
const details = [
|
|
9
|
+
typeof rateLimit.resource === "string" ? `resource ${rateLimit.resource}` : null,
|
|
9
10
|
typeof rateLimit.retryAfterSeconds === "number"
|
|
10
11
|
? `retry after ${rateLimit.retryAfterSeconds}s`
|
|
11
12
|
: null,
|
|
12
13
|
typeof rateLimit.remaining === "number" && typeof rateLimit.limit === "number"
|
|
13
14
|
? `remaining ${rateLimit.remaining}/${rateLimit.limit}`
|
|
14
15
|
: null,
|
|
16
|
+
typeof rateLimit.used === "number" ? `used ${rateLimit.used}` : null,
|
|
15
17
|
typeof rateLimit.resetAt === "number"
|
|
16
18
|
? `reset at ${new Date(rateLimit.resetAt * 1000).toISOString()}`
|
|
17
19
|
: null,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AnnotationCacheOptions } from "../github/check-annotations.mts";
|
|
1
2
|
import type { CheckAnnotation, ClassifiedCheck, ShepherdReport, TriagedCheck } from "../types.mts";
|
|
2
3
|
export declare function checksWithActionableAnnotations(report: ShepherdReport): TriagedCheck[];
|
|
3
4
|
export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
@@ -8,7 +9,7 @@ export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
|
8
9
|
ignored: ClassifiedCheck[];
|
|
9
10
|
}, seenMap: Map<string, {
|
|
10
11
|
seenAt: number;
|
|
11
|
-
}>, prNumber: number): Promise<{
|
|
12
|
+
}>, prNumber: number, cacheOpts?: AnnotationCacheOptions): Promise<{
|
|
12
13
|
passing: ClassifiedCheck[];
|
|
13
14
|
failing: TriagedCheck[];
|
|
14
15
|
skipped: ClassifiedCheck[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchCheckRunAnnotations } from "../github/check-annotations.mjs";
|
|
1
|
+
import { fetchCheckRunAnnotations, } from "../github/check-annotations.mjs";
|
|
2
2
|
function shouldFetchCheckAnnotations(check) {
|
|
3
3
|
return check.id != null && check.status === "COMPLETED" && check.hasAnnotations === true;
|
|
4
4
|
}
|
|
@@ -11,7 +11,7 @@ export function checksWithActionableAnnotations(report) {
|
|
|
11
11
|
...(report.checks.ignored ?? []),
|
|
12
12
|
].filter((c) => c.conclusion !== "SUCCESS" && (c.annotations?.length ?? 0) > 0);
|
|
13
13
|
}
|
|
14
|
-
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber) {
|
|
14
|
+
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber, cacheOpts) {
|
|
15
15
|
const candidates = [
|
|
16
16
|
...buckets.failing,
|
|
17
17
|
...buckets.passing,
|
|
@@ -19,7 +19,7 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber)
|
|
|
19
19
|
...buckets.filtered,
|
|
20
20
|
...buckets.ignored,
|
|
21
21
|
].filter(shouldFetchCheckAnnotations);
|
|
22
|
-
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber);
|
|
22
|
+
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber, cacheOpts);
|
|
23
23
|
const byId = new Map(annotated.flatMap((c) => (c.id != null ? [[c.id, c]] : [])));
|
|
24
24
|
const apply = (list) => list.map((c) => {
|
|
25
25
|
if (c.id == null)
|
|
@@ -35,20 +35,20 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber)
|
|
|
35
35
|
ignored: apply(buckets.ignored),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
async function attachUnseenCheckAnnotations(checks, seenMap, prNumber) {
|
|
38
|
+
async function attachUnseenCheckAnnotations(checks, seenMap, prNumber, cacheOpts) {
|
|
39
39
|
const checksWithAnnotations = [];
|
|
40
40
|
for (const check of checks) {
|
|
41
41
|
// eslint-disable-next-line no-await-in-loop
|
|
42
|
-
checksWithAnnotations.push(await attachForCheck(check, seenMap, prNumber));
|
|
42
|
+
checksWithAnnotations.push(await attachForCheck(check, seenMap, prNumber, cacheOpts));
|
|
43
43
|
}
|
|
44
44
|
return checksWithAnnotations;
|
|
45
45
|
}
|
|
46
|
-
async function attachForCheck(check, seenMap, prNumber) {
|
|
46
|
+
async function attachForCheck(check, seenMap, prNumber, cacheOpts) {
|
|
47
47
|
if (check.id == null)
|
|
48
48
|
return check;
|
|
49
49
|
let annotations;
|
|
50
50
|
try {
|
|
51
|
-
annotations = await fetchCheckRunAnnotations(check.id);
|
|
51
|
+
annotations = await fetchCheckRunAnnotations(check.id, cacheOpts);
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
54
|
const msg = err instanceof Error ? err.message : String(err);
|