@cldmv/gh-broker 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -41
- package/config.example.json +38 -9
- package/config.sample.json +45 -0
- package/deploy/README.md +76 -17
- package/deploy/install.mjs +171 -104
- package/deploy/install.sh +0 -0
- package/deploy/lib/config-merge.mjs +174 -16
- package/deploy/lib/credentials.mjs +143 -0
- package/deploy/lib/install-args.mjs +144 -0
- package/deploy/lib/npm-setup.mjs +164 -0
- package/deploy/lib/secret-store.mjs +20 -2
- package/deploy/lib/system.mjs +26 -2
- package/dist/index.mjs +5864 -1809
- package/dist/index.mjs.map +4 -4
- package/package.json +18 -8
- package/.configs/fix-headers.json +0 -3
- package/.configs/vitest.config.mjs +0 -44
- package/.github/dependabot.yml +0 -111
- package/.github/workflows/branch-retention.yml +0 -44
- package/.github/workflows/bundle-size.yml +0 -52
- package/.github/workflows/ci.yml +0 -389
- package/.github/workflows/cla.yml +0 -66
- package/.github/workflows/codeql.yml +0 -87
- package/.github/workflows/dependabot-auto-merge.yml +0 -62
- package/.github/workflows/dependabot-recreate.yml +0 -67
- package/.github/workflows/dependency-review.yml +0 -46
- package/.github/workflows/feature-pr.yml +0 -75
- package/.github/workflows/hotfix-redirector.yml +0 -67
- package/.github/workflows/hotfixes-release.yml +0 -70
- package/.github/workflows/labeler.yml +0 -48
- package/.github/workflows/master-commit-audit.yml +0 -56
- package/.github/workflows/member-auto-merge.yml +0 -75
- package/.github/workflows/next-release.yml +0 -76
- package/.github/workflows/next-reset.yml +0 -50
- package/.github/workflows/pr-notify.yml +0 -44
- package/.github/workflows/pr-title-normalizer.yml +0 -52
- package/.github/workflows/provenance.yml +0 -51
- package/.github/workflows/publish.yml +0 -146
- package/.github/workflows/release-merge.yml +0 -104
- package/.github/workflows/release-notify.yml +0 -41
- package/.github/workflows/scorecard.yml +0 -64
- package/.github/workflows/stale.yml +0 -50
- package/.github/workflows/tag-health.yml +0 -70
- package/.github/workflows/update-major-version-tags.yml +0 -94
- package/.github/workflows/v4-bootstrap.yml +0 -115
- package/.github/workflows/welcome.yml +0 -42
- package/docs/TOOLS.md +0 -347
- package/docs/agent-user-model.md +0 -89
- package/docs/changelog/v0/v0.2.0.md +0 -26
- package/docs/changelog/v0/v0.2.1.md +0 -23
- package/docs/changelog/v0/v0.2.2.md +0 -23
- package/docs/changelog/v0/v0.2.3.md +0 -33
- package/docs/changelog/v0/v0.2.4.md +0 -25
- package/docs/changelog/v0/v0.2.5.md +0 -29
- package/docs/changelog/v0/v0.3.0.md +0 -84
- package/docs/changelog/v0/v0.3.1.md +0 -30
- package/docs/changelog/v0/v0.3.2.md +0 -24
- package/docs/changelog/v0/v0.3.3.md +0 -28
- package/docs/changelog/v0/v0.4.0.md +0 -58
- package/docs/changelog/v0/v0.4.1.md +0 -33
- package/docs/changelog/v0/v0.4.2.md +0 -45
- package/docs/changelog/v0/v0.5.0.md +0 -39
- package/docs/changelog/v0/v0.5.1.md +0 -28
- package/docs/changelog/v0/v0.6.0.md +0 -52
- package/docs/changelog/v0/v0.6.1.md +0 -48
- package/docs/changelog/v0/v0.7.0.md +0 -71
- package/docs/deploy-architecture.md +0 -82
- package/src/appauth.ts +0 -56
- package/src/audit.ts +0 -34
- package/src/catalog.ts +0 -165
- package/src/config.ts +0 -130
- package/src/index.ts +0 -107
- package/src/paths.ts +0 -60
- package/src/router.ts +0 -258
- package/src/secrets.ts +0 -137
- package/src/server.ts +0 -91
- package/src/types.ts +0 -104
- package/src/upstream.ts +0 -85
- package/src/verbs/issue_labels.ts +0 -329
- package/src/verbs/link_issue.ts +0 -84
- package/src/verbs/release-analysis.ts +0 -210
- package/src/verbs/repo-tier.ts +0 -90
- package/src/verbs/report_ci_status.ts +0 -358
- package/src/verbs/report_release_prs.ts +0 -238
- package/src/verbs/report_security_alerts.ts +0 -442
- package/src/verbs/report_unlabeled.ts +0 -194
- package/src/verbs/rest-client.ts +0 -181
- package/src/verbs/review_thread.ts +0 -138
- package/tests/appauth.test.vitest.mts +0 -76
- package/tests/audit.test.vitest.mts +0 -48
- package/tests/catalog.test.vitest.mts +0 -180
- package/tests/config-merge.test.vitest.mts +0 -58
- package/tests/config.test.vitest.mts +0 -314
- package/tests/issue-labels.test.vitest.mts +0 -347
- package/tests/paths.test.vitest.mts +0 -52
- package/tests/release-prs.test.vitest.mts +0 -237
- package/tests/rest-client.test.vitest.mts +0 -115
- package/tests/router.test.vitest.mts +0 -399
- package/tests/run-vitest.mjs +0 -51
- package/tests/secret-store.test.vitest.mts +0 -106
- package/tests/secrets.test.vitest.mts +0 -155
- package/tests/security-alerts.test.vitest.mts +0 -328
- package/tests/server.test.vitest.mts +0 -20
- package/tests/upstream.test.vitest.mts +0 -91
- package/tests/verb-helpers.test.vitest.mts +0 -331
- package/tests/verbs.test.vitest.mts +0 -624
- package/tsconfig.json +0 -21
package/src/verbs/repo-tier.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* @Project: @cldmv/gh-broker
|
|
4
|
-
* @Filename: /src/verbs/repo-tier.ts
|
|
5
|
-
* @Date: 2026-10-04T15:40:09-07:00 (1791153609)
|
|
6
|
-
* @Author: Nate Corcoran <CLDMV>
|
|
7
|
-
* @Email: <Shinrai@users.noreply.github.com>
|
|
8
|
-
* -----
|
|
9
|
-
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
|
|
10
|
-
* @Last modified time: 2026-10-04T16:41:24-07:00 (1791157284)
|
|
11
|
-
* -----
|
|
12
|
-
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Classifies a repo into the CLDMV flow tiers the maintenance skills care about.
|
|
18
|
-
*
|
|
19
|
-
* - `v4` — carries the v4 release workflow and is a normal next → master staging-flow repo.
|
|
20
|
-
* - `dev-trunk` — a v4 repo whose real code lives on a persistent `dev` branch while the default
|
|
21
|
-
* branch / `next` hold only the scaffold.
|
|
22
|
-
* - `non-v4` — no v4 release workflow on the default branch (even if `next`/`hotfixes` exist).
|
|
23
|
-
*
|
|
24
|
-
* Detection is structural, from three cheap reads, so a skill doesn't have to walk branches and
|
|
25
|
-
* workflow folders itself. It is a signal, not a verdict: a stale `dev` branch on a repo whose
|
|
26
|
-
* `next` happens to equal `master` right after a release looks like a dev-trunk repo, so the
|
|
27
|
-
* raw `signals` are returned for the caller to cross-check.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import type { Octokit } from "@octokit/core";
|
|
31
|
-
import { CallBudget, CallBudgetExhausted, RateLimited, reqOne } from "./rest-client.ts";
|
|
32
|
-
|
|
33
|
-
export type RepoTierName = "v4" | "dev-trunk" | "non-v4";
|
|
34
|
-
|
|
35
|
-
export interface RepoTier {
|
|
36
|
-
tier: RepoTierName;
|
|
37
|
-
signals: {
|
|
38
|
-
/** `.github/workflows/next-release.yml` exists on the default branch. */
|
|
39
|
-
v4Workflow: boolean;
|
|
40
|
-
defaultBranch?: string;
|
|
41
|
-
/** `next` branch exists. */
|
|
42
|
-
next?: boolean;
|
|
43
|
-
/** `dev` branch exists. */
|
|
44
|
-
dev?: boolean;
|
|
45
|
-
/** `next` is at the same commit as the default branch (nothing queued; scaffold-only when dev exists). */
|
|
46
|
-
nextEqualsDefault?: boolean;
|
|
47
|
-
/** `dev` is at a different commit than the default branch. */
|
|
48
|
-
devDiffersFromDefault?: boolean;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function getOrUndefined<T>(octokit: Octokit, budget: CallBudget, route: string, params: Record<string, unknown>): Promise<T | undefined> {
|
|
53
|
-
try {
|
|
54
|
-
return await reqOne<T>(octokit, budget, route, params);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
if (err instanceof RateLimited || err instanceof CallBudgetExhausted) throw err;
|
|
57
|
-
if ((err as { status?: number }).status === 404) return undefined;
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function classifyRepo(octokit: Octokit, budget: CallBudget, owner: string, repo: string): Promise<RepoTier> {
|
|
63
|
-
const info = await getOrUndefined<{ default_branch?: string }>(octokit, budget, "GET /repos/{owner}/{repo}", { owner, repo });
|
|
64
|
-
const defaultBranch = info?.default_branch;
|
|
65
|
-
const workflow = await getOrUndefined<unknown>(octokit, budget, "GET /repos/{owner}/{repo}/contents/{path}", {
|
|
66
|
-
owner,
|
|
67
|
-
repo,
|
|
68
|
-
path: ".github/workflows/next-release.yml",
|
|
69
|
-
...(defaultBranch ? { ref: defaultBranch } : {})
|
|
70
|
-
});
|
|
71
|
-
const v4Workflow = workflow !== undefined;
|
|
72
|
-
if (!v4Workflow) return { tier: "non-v4", signals: { v4Workflow, ...(defaultBranch ? { defaultBranch } : {}) } };
|
|
73
|
-
|
|
74
|
-
const sha = async (branch: string): Promise<string | undefined> => {
|
|
75
|
-
const b = await getOrUndefined<{ commit?: { sha?: string } }>(octokit, budget, "GET /repos/{owner}/{repo}/branches/{branch}", { owner, repo, branch });
|
|
76
|
-
return b?.commit?.sha;
|
|
77
|
-
};
|
|
78
|
-
const [defaultSha, nextSha, devSha] = await Promise.all([defaultBranch ? sha(defaultBranch) : undefined, sha("next"), sha("dev")]);
|
|
79
|
-
const signals: RepoTier["signals"] = {
|
|
80
|
-
v4Workflow,
|
|
81
|
-
...(defaultBranch ? { defaultBranch } : {}),
|
|
82
|
-
next: nextSha !== undefined,
|
|
83
|
-
dev: devSha !== undefined,
|
|
84
|
-
...(nextSha && defaultSha ? { nextEqualsDefault: nextSha === defaultSha } : {}),
|
|
85
|
-
...(devSha && defaultSha ? { devDiffersFromDefault: devSha !== defaultSha } : {})
|
|
86
|
-
};
|
|
87
|
-
const scaffoldOnly = nextSha === undefined || nextSha === defaultSha;
|
|
88
|
-
const tier: RepoTierName = devSha !== undefined && signals.devDiffersFromDefault && scaffoldOnly ? "dev-trunk" : "v4";
|
|
89
|
-
return { tier, signals };
|
|
90
|
-
}
|
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* @Project: @cldmv/gh-broker
|
|
4
|
-
* @Filename: /src/verbs/report_ci_status.ts
|
|
5
|
-
* @Date: 2026-09-29T02:39:40+00:00 (1790649580)
|
|
6
|
-
* @Author: Nate Corcoran <CLDMV>
|
|
7
|
-
* @Email: <Shinrai@users.noreply.github.com>
|
|
8
|
-
* -----
|
|
9
|
-
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
|
|
10
|
-
* @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
|
|
11
|
-
* -----
|
|
12
|
-
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { Octokit } from "@octokit/core";
|
|
17
|
-
import { CallBudget, CallBudgetExhausted, RateLimited, paginate, reqOne } from "./rest-client.ts";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Custom verb: report_ci_status — CI health for a PR (or every open PR in a repo),
|
|
21
|
-
* at a verbosity the caller chooses: counts only by default, escalating to
|
|
22
|
-
* run/job detail, failing-step detail, and finally a tail of the failing job's log.
|
|
23
|
-
*
|
|
24
|
-
* Run/job/step classification is built on the GitHub Actions REST API
|
|
25
|
-
* (`/actions/runs`, `/actions/runs/{id}/jobs`), NOT the Checks API
|
|
26
|
-
* (`get_check_runs`, already proxied via `pull_request_read`) — verified live that
|
|
27
|
-
* the configured read PAT has Actions access but not Checks access (fine-grained
|
|
28
|
-
* PAT permissions are separate scopes). Building on Actions avoids needing a PAT
|
|
29
|
-
* permission change to ship this.
|
|
30
|
-
*
|
|
31
|
-
* Tier 3's log fetch instead goes through the hosted MCP's own `get_job_logs` tool
|
|
32
|
-
* (`fetchFailedJobLogs`, injected by the caller — router.ts has the upstream/token
|
|
33
|
-
* this verb doesn't) rather than a hand-rolled REST call: `get_job_logs` already
|
|
34
|
-
* does exactly this (`run_id` + `failed_only: true` fetches every failing job in a
|
|
35
|
-
* run in one call, with its own `tail_lines`), so there's no reason to duplicate
|
|
36
|
-
* it. One call per failing RUN, not per failing job. This means tier-3 log fetches
|
|
37
|
-
* are NOT counted by the CallBudget below (that only tracks direct `octokit.request`
|
|
38
|
-
* calls and their rate-limit headers) — an accepted gap, not an oversight: an
|
|
39
|
-
* upstream-forwarded MCP call exposes no rate-limit headers to inspect, and the
|
|
40
|
-
* fan-out is already bounded (one call per ACTUALLY-failing run, not per PR).
|
|
41
|
-
*
|
|
42
|
-
* Classification (run/job/step all share this): `status !== "completed"` is
|
|
43
|
-
* "pending"; a completed run/job/step is "failed" unless its conclusion is
|
|
44
|
-
* success/neutral/skipped (those three are non-blocking outcomes, not failures).
|
|
45
|
-
*
|
|
46
|
-
* Fan-out: verbosity >= 1 detail (job/step calls, and >= 3's log fetch) is only
|
|
47
|
-
* ever spent on runs/jobs that are ACTUALLY failing — a healthy PR costs exactly
|
|
48
|
-
* one call regardless of verbosity. When `pull` is omitted, only PRs with at least
|
|
49
|
-
* one failure are included in the result (see rest-client.ts for the shared
|
|
50
|
-
* call-budget + rate-limit-floor mechanics, identical to report_unlabeled's).
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
export type Verbosity = 0 | 1 | 2 | 3;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Fetches every failing job's log (already tailed to `tailLines`) for one workflow
|
|
57
|
-
* run, keyed by job id — the shape `get_job_logs` returns with `failed_only: true`.
|
|
58
|
-
* Injected by the caller (router.ts), which holds the upstream/token this verb
|
|
59
|
-
* doesn't. Absent (or a lookup miss for a given job) just means no `logTail` on
|
|
60
|
-
* that failure — best-effort, never a hard failure of the whole report.
|
|
61
|
-
*/
|
|
62
|
-
export type FailedJobLogsFetcher = (runId: number, tailLines: number) => Promise<Map<number, string>>;
|
|
63
|
-
|
|
64
|
-
export interface ReportCiStatusInput {
|
|
65
|
-
owner: string;
|
|
66
|
-
repo: string;
|
|
67
|
-
/** PR number; omit to scan every open PR in the repo. */
|
|
68
|
-
pull?: number;
|
|
69
|
-
verbosity?: Verbosity;
|
|
70
|
-
/** Tier-3 only: tail lines of the failing job's log. Default 200, hard cap 2000. */
|
|
71
|
-
logLines?: number;
|
|
72
|
-
/** Safety cap on open PRs scanned when `pull` is omitted. Default 50, hard cap 200. */
|
|
73
|
-
maxPulls?: number;
|
|
74
|
-
/** Hard ceiling on total REST calls for this invocation. Default 250, hard cap 1000. */
|
|
75
|
-
maxApiCalls?: number;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface CiSummary {
|
|
79
|
-
passed: number;
|
|
80
|
-
failed: number;
|
|
81
|
-
pending: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface CiStepFailure {
|
|
85
|
-
name: string;
|
|
86
|
-
number: number;
|
|
87
|
-
conclusion: string;
|
|
88
|
-
durationMs: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface CiFailure {
|
|
92
|
-
workflow: string;
|
|
93
|
-
job: string;
|
|
94
|
-
runId: number;
|
|
95
|
-
runUrl: string;
|
|
96
|
-
conclusion: string;
|
|
97
|
-
/** Tier >= 2: the job's failing steps. */
|
|
98
|
-
steps?: CiStepFailure[];
|
|
99
|
-
/**
|
|
100
|
-
* Tier 3: tail of the JOB's combined log (not per-step — GitHub's job log is one
|
|
101
|
-
* concatenated stream across all steps; reliably slicing it per-step would need
|
|
102
|
-
* parsing timestamp/group markers, which is fragile. The job-level tail almost
|
|
103
|
-
* always contains the actual failure output anyway.
|
|
104
|
-
*/
|
|
105
|
-
logTail?: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export type TruncatedReason = "max-pulls" | "max-api-calls" | "rate-limited";
|
|
109
|
-
|
|
110
|
-
interface Truncation {
|
|
111
|
-
truncated: true;
|
|
112
|
-
truncatedReason: TruncatedReason;
|
|
113
|
-
retryAfterSeconds?: number;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface CiStatusResult extends Partial<Truncation> {
|
|
117
|
-
pull: number;
|
|
118
|
-
summary: CiSummary;
|
|
119
|
-
failures?: CiFailure[];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface ReportCiStatusResult extends Partial<Truncation> {
|
|
123
|
-
scanned: { pulls: number };
|
|
124
|
-
results: CiStatusResult[];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const DEFAULT_LOG_LINES = 200;
|
|
128
|
-
const HARD_LOG_LINES = 2000;
|
|
129
|
-
const DEFAULT_MAX_PULLS = 50;
|
|
130
|
-
const HARD_MAX_PULLS = 200;
|
|
131
|
-
const DEFAULT_MAX_API_CALLS = 250;
|
|
132
|
-
const HARD_MAX_API_CALLS = 1000;
|
|
133
|
-
|
|
134
|
-
interface RawRun {
|
|
135
|
-
id: number;
|
|
136
|
-
name: string;
|
|
137
|
-
status: string;
|
|
138
|
-
conclusion: string | null;
|
|
139
|
-
html_url: string;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
interface RawStep {
|
|
143
|
-
name: string;
|
|
144
|
-
status: string;
|
|
145
|
-
conclusion: string | null;
|
|
146
|
-
number: number;
|
|
147
|
-
started_at: string | null;
|
|
148
|
-
completed_at: string | null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
interface RawJob {
|
|
152
|
-
id: number;
|
|
153
|
-
name: string;
|
|
154
|
-
status: string;
|
|
155
|
-
conclusion: string | null;
|
|
156
|
-
html_url: string;
|
|
157
|
-
steps?: RawStep[];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
type Outcome = "passed" | "failed" | "pending";
|
|
161
|
-
|
|
162
|
-
/** Non-blocking completed conclusions — everything else completed counts as failed. */
|
|
163
|
-
const NON_FAILING_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
164
|
-
|
|
165
|
-
function classify(status: string, conclusion: string | null): Outcome {
|
|
166
|
-
if (status !== "completed") return "pending";
|
|
167
|
-
return conclusion !== null && NON_FAILING_CONCLUSIONS.has(conclusion) ? "passed" : "failed";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function stepDurationMs(step: RawStep): number {
|
|
171
|
-
if (!step.started_at || !step.completed_at) return 0;
|
|
172
|
-
return new Date(step.completed_at).getTime() - new Date(step.started_at).getTime();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function truncationFrom(err: unknown): Truncation | undefined {
|
|
176
|
-
if (err instanceof RateLimited) {
|
|
177
|
-
return { truncated: true, truncatedReason: "rate-limited", ...(err.retryAfterSeconds !== undefined ? { retryAfterSeconds: err.retryAfterSeconds } : {}) };
|
|
178
|
-
}
|
|
179
|
-
if (err instanceof CallBudgetExhausted) return { truncated: true, truncatedReason: "max-api-calls" };
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function computeForPull(
|
|
184
|
-
octokit: Octokit,
|
|
185
|
-
budget: CallBudget,
|
|
186
|
-
owner: string,
|
|
187
|
-
repo: string,
|
|
188
|
-
pullNumber: number,
|
|
189
|
-
headSha: string,
|
|
190
|
-
verbosity: Verbosity,
|
|
191
|
-
logLines: number,
|
|
192
|
-
fetchFailedJobLogs?: FailedJobLogsFetcher
|
|
193
|
-
): Promise<CiStatusResult> {
|
|
194
|
-
const summary: CiSummary = { passed: 0, failed: 0, pending: 0 };
|
|
195
|
-
const failures: CiFailure[] = [];
|
|
196
|
-
try {
|
|
197
|
-
const runs = await paginate<RawRun>(
|
|
198
|
-
octokit,
|
|
199
|
-
budget,
|
|
200
|
-
"GET /repos/{owner}/{repo}/actions/runs",
|
|
201
|
-
{ owner, repo, head_sha: headSha },
|
|
202
|
-
100,
|
|
203
|
-
(d) => (d as { workflow_runs: RawRun[] }).workflow_runs
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const failedRuns: RawRun[] = [];
|
|
207
|
-
for (const run of runs) {
|
|
208
|
-
const outcome = classify(run.status, run.conclusion);
|
|
209
|
-
summary[outcome]++;
|
|
210
|
-
if (outcome === "failed") failedRuns.push(run);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (verbosity >= 1) {
|
|
214
|
-
for (const run of failedRuns) {
|
|
215
|
-
const jobs = await paginate<RawJob>(
|
|
216
|
-
octokit,
|
|
217
|
-
budget,
|
|
218
|
-
"GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs",
|
|
219
|
-
{ owner, repo, run_id: run.id },
|
|
220
|
-
100,
|
|
221
|
-
(d) => (d as { jobs: RawJob[] }).jobs
|
|
222
|
-
);
|
|
223
|
-
// One get_job_logs call per FAILING RUN (not per job) — failed_only:true
|
|
224
|
-
// already returns every failing job's log in that run at once.
|
|
225
|
-
const logsByJobId = verbosity >= 3 && fetchFailedJobLogs ? await fetchFailedJobLogs(run.id, logLines) : undefined;
|
|
226
|
-
for (const job of jobs) {
|
|
227
|
-
if (classify(job.status, job.conclusion) !== "failed") continue;
|
|
228
|
-
const failure: CiFailure = { workflow: run.name, job: job.name, runId: run.id, runUrl: run.html_url, conclusion: job.conclusion ?? "unknown" };
|
|
229
|
-
if (verbosity >= 2) {
|
|
230
|
-
failure.steps = (job.steps ?? [])
|
|
231
|
-
.filter((s) => classify(s.status, s.conclusion) === "failed")
|
|
232
|
-
.map((s) => ({ name: s.name, number: s.number, conclusion: s.conclusion ?? "unknown", durationMs: stepDurationMs(s) }));
|
|
233
|
-
}
|
|
234
|
-
const logTail = logsByJobId?.get(job.id);
|
|
235
|
-
if (logTail !== undefined) failure.logTail = logTail;
|
|
236
|
-
failures.push(failure);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} catch (err) {
|
|
241
|
-
const t = truncationFrom(err);
|
|
242
|
-
if (t) return { pull: pullNumber, summary, ...(failures.length ? { failures } : {}), ...t };
|
|
243
|
-
throw err;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const result: CiStatusResult = { pull: pullNumber, summary };
|
|
247
|
-
if (verbosity >= 1) result.failures = failures;
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function clampVerbosity(v: number | undefined): Verbosity {
|
|
252
|
-
return Math.min(Math.max(v ?? 0, 0), 3) as Verbosity;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* CI status for a PR whose head sha is already known — skips the PR lookup
|
|
257
|
-
* `reportCiStatus` would otherwise make. Used by router.ts's `pull_request_read`
|
|
258
|
-
* augmentation, which already has the PR (and its head sha) from the forwarded
|
|
259
|
-
* upstream response.
|
|
260
|
-
*/
|
|
261
|
-
export async function ciStatusForHeadSha(
|
|
262
|
-
octokit: Octokit,
|
|
263
|
-
owner: string,
|
|
264
|
-
repo: string,
|
|
265
|
-
pullNumber: number,
|
|
266
|
-
headSha: string,
|
|
267
|
-
opts: { verbosity?: number; logLines?: number; maxApiCalls?: number; fetchFailedJobLogs?: FailedJobLogsFetcher } = {}
|
|
268
|
-
): Promise<CiStatusResult> {
|
|
269
|
-
const budget = new CallBudget(Math.min(opts.maxApiCalls ?? DEFAULT_MAX_API_CALLS, HARD_MAX_API_CALLS));
|
|
270
|
-
return computeForPull(
|
|
271
|
-
octokit,
|
|
272
|
-
budget,
|
|
273
|
-
owner,
|
|
274
|
-
repo,
|
|
275
|
-
pullNumber,
|
|
276
|
-
headSha,
|
|
277
|
-
clampVerbosity(opts.verbosity),
|
|
278
|
-
Math.min(opts.logLines ?? DEFAULT_LOG_LINES, HARD_LOG_LINES),
|
|
279
|
-
opts.fetchFailedJobLogs
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export async function reportCiStatus(
|
|
284
|
-
octokit: Octokit,
|
|
285
|
-
input: ReportCiStatusInput,
|
|
286
|
-
fetchFailedJobLogs?: FailedJobLogsFetcher
|
|
287
|
-
): Promise<CiStatusResult | ReportCiStatusResult> {
|
|
288
|
-
const verbosity = clampVerbosity(input.verbosity);
|
|
289
|
-
const logLines = Math.min(input.logLines ?? DEFAULT_LOG_LINES, HARD_LOG_LINES);
|
|
290
|
-
const maxApiCalls = Math.min(input.maxApiCalls ?? DEFAULT_MAX_API_CALLS, HARD_MAX_API_CALLS);
|
|
291
|
-
const budget = new CallBudget(maxApiCalls);
|
|
292
|
-
|
|
293
|
-
if (input.pull !== undefined) {
|
|
294
|
-
const pullNumber = input.pull;
|
|
295
|
-
try {
|
|
296
|
-
const pr = await reqOne<{ head: { sha: string } }>(octokit, budget, "GET /repos/{owner}/{repo}/pulls/{pull_number}", {
|
|
297
|
-
owner: input.owner,
|
|
298
|
-
repo: input.repo,
|
|
299
|
-
pull_number: pullNumber
|
|
300
|
-
});
|
|
301
|
-
return await computeForPull(octokit, budget, input.owner, input.repo, pullNumber, pr.head.sha, verbosity, logLines, fetchFailedJobLogs);
|
|
302
|
-
} catch (err) {
|
|
303
|
-
const t = truncationFrom(err);
|
|
304
|
-
if (t) return { pull: pullNumber, summary: { passed: 0, failed: 0, pending: 0 }, ...t };
|
|
305
|
-
throw err;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const maxPulls = Math.min(input.maxPulls ?? DEFAULT_MAX_PULLS, HARD_MAX_PULLS);
|
|
310
|
-
let pulls: Array<{ number: number; head: { sha: string } }>;
|
|
311
|
-
try {
|
|
312
|
-
pulls = await paginate(octokit, budget, "GET /repos/{owner}/{repo}/pulls", { owner: input.owner, repo: input.repo, state: "open" }, maxPulls);
|
|
313
|
-
} catch (err) {
|
|
314
|
-
const t = truncationFrom(err);
|
|
315
|
-
if (t) return { scanned: { pulls: 0 }, results: [], ...t };
|
|
316
|
-
throw err;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const results: CiStatusResult[] = [];
|
|
320
|
-
let scanned = 0;
|
|
321
|
-
let aggregateTruncation: Truncation | undefined;
|
|
322
|
-
for (const p of pulls) {
|
|
323
|
-
const r = await computeForPull(octokit, budget, input.owner, input.repo, p.number, p.head.sha, verbosity, logLines, fetchFailedJobLogs);
|
|
324
|
-
if (r.truncated) {
|
|
325
|
-
// Didn't finish evaluating this PR — don't count it as scanned, but surface
|
|
326
|
-
// it (and stop entirely: the budget/rate limit is gone for the next PR too).
|
|
327
|
-
results.push(r);
|
|
328
|
-
aggregateTruncation = { truncated: true, truncatedReason: r.truncatedReason as TruncatedReason, ...(r.retryAfterSeconds !== undefined ? { retryAfterSeconds: r.retryAfterSeconds } : {}) };
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
scanned++;
|
|
332
|
-
if (r.summary.failed > 0) results.push(r);
|
|
333
|
-
}
|
|
334
|
-
if (!aggregateTruncation && pulls.length === maxPulls) aggregateTruncation = { truncated: true, truncatedReason: "max-pulls" };
|
|
335
|
-
|
|
336
|
-
return { scanned: { pulls: scanned }, results, ...aggregateTruncation };
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** JSON Schema advertised to the agent for report_ci_status. */
|
|
340
|
-
export const REPORT_CI_STATUS_SCHEMA = {
|
|
341
|
-
type: "object" as const,
|
|
342
|
-
required: ["owner", "repo"],
|
|
343
|
-
additionalProperties: false,
|
|
344
|
-
properties: {
|
|
345
|
-
owner: { type: "string", description: "repository owner (org/user login) — selects the read credential" },
|
|
346
|
-
repo: { type: "string", description: "repository name" },
|
|
347
|
-
pull: { type: "number", description: "PR number; omit to scan every open PR in the repo (only PRs with a failure are included in the result)" },
|
|
348
|
-
verbosity: {
|
|
349
|
-
type: "number",
|
|
350
|
-
enum: [0, 1, 2, 3],
|
|
351
|
-
default: 0,
|
|
352
|
-
description: "0: pass/fail/pending counts only. 1: + which run/job failed (workflow, job, runId, runUrl). 2: + which step in that job failed. 3: + a tail of the failing job's log."
|
|
353
|
-
},
|
|
354
|
-
logLines: { type: "number", description: `tier-3 only: tail lines of the failing job's log (default ${DEFAULT_LOG_LINES}, hard cap ${HARD_LOG_LINES})` },
|
|
355
|
-
maxPulls: { type: "number", description: `safety cap on open PRs scanned when pull is omitted (default ${DEFAULT_MAX_PULLS}, hard cap ${HARD_MAX_PULLS})` },
|
|
356
|
-
maxApiCalls: { type: "number", description: `hard ceiling on total REST calls this invocation makes (default ${DEFAULT_MAX_API_CALLS}, hard cap ${HARD_MAX_API_CALLS})` }
|
|
357
|
-
}
|
|
358
|
-
};
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* @Project: @cldmv/gh-broker
|
|
4
|
-
* @Filename: /src/verbs/report_release_prs.ts
|
|
5
|
-
* @Date: 2026-10-03T15:00:00+00:00 (1791039600)
|
|
6
|
-
* @Author: Nate Corcoran <CLDMV>
|
|
7
|
-
* @Email: <Shinrai@users.noreply.github.com>
|
|
8
|
-
* -----
|
|
9
|
-
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
|
|
10
|
-
* @Last modified time: 2026-10-03T15:00:00+00:00 (1791039600)
|
|
11
|
-
* -----
|
|
12
|
-
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { Octokit } from "@octokit/core";
|
|
17
|
-
import { CallBudget, CallBudgetExhausted, RateLimited, reqOne } from "./rest-client.ts";
|
|
18
|
-
import { analyzeRelease, type ReleaseAnalysis, type ReleaseVerdict } from "./release-analysis.ts";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Custom verb: report_release_prs — every open release PR for an owner (or one repo),
|
|
22
|
-
* with whether its version's changelog file is already on the release branch.
|
|
23
|
-
*
|
|
24
|
-
* Built for mass updates: an agent sweeping a fleet needs to know which repos have a
|
|
25
|
-
* release PR waiting and which of those still lack the curated per-version changelog
|
|
26
|
-
* (`docs/changelog[s]/v<major>/vX.Y.Z.md`, the exact file the CLDMV release automation
|
|
27
|
-
* injects as the release-PR body). Per-repo `list_pull_requests` over every repo would
|
|
28
|
-
* be hundreds of calls; this uses ONE search call for the whole owner plus two cheap
|
|
29
|
-
* calls per release PR actually found.
|
|
30
|
-
*
|
|
31
|
-
* A "release PR" is an open PR carrying the `release` label (applied by the v4 release
|
|
32
|
-
* automation to both the `next → master` and `hotfixes → master` PRs). The version is
|
|
33
|
-
* read from the `release: vX.Y.Z - …` title.
|
|
34
|
-
*
|
|
35
|
-
* The changelog is looked up at the PR's head commit, not the default branch — the file
|
|
36
|
-
* lands on `next` before the release PR merges, so that is where it must exist.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
export interface ReportReleasePrsInput {
|
|
40
|
-
owner: string;
|
|
41
|
-
/** Repo name; omit to search every repo this owner has. */
|
|
42
|
-
repo?: string;
|
|
43
|
-
/** Return only release PRs that are not ready (missing changelog or unreadable version). Default false. */
|
|
44
|
-
onlyMissing?: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* Also work out, per release PR, what is still open against its release branch, whether an existing
|
|
47
|
-
* changelog is actually up to date, and a verdict (done / in-flight / wait / gray / write / update / inspect).
|
|
48
|
-
* Costs about 5 more REST calls per release PR, so raise `maxApiCalls` for a whole org. Default false.
|
|
49
|
-
*/
|
|
50
|
-
analyze?: boolean;
|
|
51
|
-
/** Hard ceiling on total REST calls for this invocation. Default 300, hard-capped at 1000. */
|
|
52
|
-
maxApiCalls?: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type ReleasePrStatus = "ready" | "needs-changelog" | "unknown-version";
|
|
56
|
-
|
|
57
|
-
export interface ReleasePr {
|
|
58
|
-
repo: string;
|
|
59
|
-
pull: number;
|
|
60
|
-
url: string;
|
|
61
|
-
title: string;
|
|
62
|
-
/** Parsed from the `release: vX.Y.Z` title; null when the title has no version. */
|
|
63
|
-
version: string | null;
|
|
64
|
-
head: string;
|
|
65
|
-
base: string;
|
|
66
|
-
draft: boolean;
|
|
67
|
-
status: ReleasePrStatus;
|
|
68
|
-
/** The changelog path found, when `status` is "ready". */
|
|
69
|
-
changelogPath?: string;
|
|
70
|
-
/** The paths that were looked up, when `status` is "needs-changelog". */
|
|
71
|
-
checkedPaths?: string[];
|
|
72
|
-
/** Only with `analyze: true`. */
|
|
73
|
-
analysis?: ReleaseAnalysis;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type ReleaseTruncatedReason = "max-results" | "incomplete-search" | "max-api-calls" | "rate-limited";
|
|
77
|
-
|
|
78
|
-
export interface ReportReleasePrsResult {
|
|
79
|
-
scanned: { releasePrs: number };
|
|
80
|
-
summary: { ready: number; needsChangelog: number; unknownVersion: number };
|
|
81
|
-
/** Only with `analyze: true`: how many release PRs landed on each verdict. */
|
|
82
|
-
verdicts?: Partial<Record<ReleaseVerdict, number>>;
|
|
83
|
-
releasePrs: ReleasePr[];
|
|
84
|
-
truncated?: boolean;
|
|
85
|
-
truncatedReason?: ReleaseTruncatedReason;
|
|
86
|
-
/** Only set when truncatedReason is "rate-limited" and GitHub sent a Retry-After. */
|
|
87
|
-
retryAfterSeconds?: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const DEFAULT_MAX_API_CALLS = 300;
|
|
91
|
-
const HARD_MAX_API_CALLS = 1000;
|
|
92
|
-
const SEARCH_PAGE_SIZE = 100;
|
|
93
|
-
|
|
94
|
-
interface SearchHit {
|
|
95
|
-
number: number;
|
|
96
|
-
title: string;
|
|
97
|
-
html_url: string;
|
|
98
|
-
draft?: boolean;
|
|
99
|
-
repository_url: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
interface SearchResponse {
|
|
103
|
-
total_count: number;
|
|
104
|
-
incomplete_results: boolean;
|
|
105
|
-
items: SearchHit[];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
interface RawPull {
|
|
109
|
-
head: { ref: string };
|
|
110
|
-
base: { ref: string };
|
|
111
|
-
draft?: boolean;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Pulls `1.2.3` (or a prerelease form) out of a `release: v1.2.3 - …` title. */
|
|
115
|
-
export function parseReleaseVersion(title: string): string | null {
|
|
116
|
-
const m = /\brelease:\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/i.exec(title);
|
|
117
|
-
return m ? m[1]! : null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** The places a version's changelog can live — `changelog` and `changelogs` are both in use. */
|
|
121
|
-
export function changelogPaths(version: string): string[] {
|
|
122
|
-
const major = version.split(".")[0]!;
|
|
123
|
-
return [`docs/changelog/v${major}/v${version}.md`, `docs/changelogs/v${major}/v${version}.md`];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function repoFromUrl(repositoryUrl: string): { owner: string; repo: string } {
|
|
127
|
-
const parts = repositoryUrl.split("/");
|
|
128
|
-
return { owner: parts[parts.length - 2]!, repo: parts[parts.length - 1]! };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** First of `paths` that exists at `ref`, or undefined. A 404 means "not there"; any other error propagates. */
|
|
132
|
-
async function findChangelog(octokit: Octokit, budget: CallBudget, owner: string, repo: string, ref: string, paths: string[]): Promise<string | undefined> {
|
|
133
|
-
for (const path of paths) {
|
|
134
|
-
try {
|
|
135
|
-
await reqOne(octokit, budget, "GET /repos/{owner}/{repo}/contents/{path}", { owner, repo, path, ref });
|
|
136
|
-
return path;
|
|
137
|
-
} catch (err) {
|
|
138
|
-
if (err instanceof RateLimited || err instanceof CallBudgetExhausted) throw err;
|
|
139
|
-
if ((err as { status?: number }).status !== 404) throw err;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function reportReleasePrs(octokit: Octokit, input: ReportReleasePrsInput): Promise<ReportReleasePrsResult> {
|
|
146
|
-
const maxApiCalls = Math.min(input.maxApiCalls ?? DEFAULT_MAX_API_CALLS, HARD_MAX_API_CALLS);
|
|
147
|
-
const budget = new CallBudget(maxApiCalls);
|
|
148
|
-
const scope = input.repo ? `repo:${input.owner}/${input.repo}` : `org:${input.owner}`;
|
|
149
|
-
|
|
150
|
-
const releasePrs: ReleasePr[] = [];
|
|
151
|
-
let truncatedReason: ReleaseTruncatedReason | undefined;
|
|
152
|
-
let retryAfterSeconds: number | undefined;
|
|
153
|
-
let hits = 0;
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const search = await reqOne<SearchResponse>(octokit, budget, "GET /search/issues", {
|
|
157
|
-
q: `${scope} is:pr is:open label:release`,
|
|
158
|
-
per_page: SEARCH_PAGE_SIZE
|
|
159
|
-
});
|
|
160
|
-
hits = search.items.length;
|
|
161
|
-
if (search.total_count > search.items.length) truncatedReason = "max-results";
|
|
162
|
-
else if (search.incomplete_results) truncatedReason = "incomplete-search";
|
|
163
|
-
|
|
164
|
-
for (const hit of search.items) {
|
|
165
|
-
const { owner, repo } = repoFromUrl(hit.repository_url);
|
|
166
|
-
const pull = await reqOne<RawPull>(octokit, budget, "GET /repos/{owner}/{repo}/pulls/{pull_number}", { owner, repo, pull_number: hit.number });
|
|
167
|
-
const version = parseReleaseVersion(hit.title);
|
|
168
|
-
const base = {
|
|
169
|
-
repo: `${owner}/${repo}`,
|
|
170
|
-
pull: hit.number,
|
|
171
|
-
url: hit.html_url,
|
|
172
|
-
title: hit.title,
|
|
173
|
-
version,
|
|
174
|
-
head: pull.head.ref,
|
|
175
|
-
base: pull.base.ref,
|
|
176
|
-
draft: pull.draft ?? hit.draft ?? false
|
|
177
|
-
};
|
|
178
|
-
let entry: ReleasePr;
|
|
179
|
-
if (version === null) {
|
|
180
|
-
entry = { ...base, status: "unknown-version" };
|
|
181
|
-
} else {
|
|
182
|
-
const paths = changelogPaths(version);
|
|
183
|
-
const found = await findChangelog(octokit, budget, owner, repo, pull.head.ref, paths);
|
|
184
|
-
entry = found ? { ...base, status: "ready", changelogPath: found } : { ...base, status: "needs-changelog", checkedPaths: paths };
|
|
185
|
-
}
|
|
186
|
-
// Pushed before the analysis so a cap hit mid-analysis still reports this PR's status.
|
|
187
|
-
releasePrs.push(entry);
|
|
188
|
-
if (input.analyze) {
|
|
189
|
-
entry.analysis = await analyzeRelease(octokit, budget, { owner, repo, pull: hit.number, head: pull.head.ref, version, ...(entry.changelogPath ? { changelogPath: entry.changelogPath } : {}) });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
} catch (err) {
|
|
193
|
-
if (err instanceof RateLimited) {
|
|
194
|
-
truncatedReason = "rate-limited";
|
|
195
|
-
retryAfterSeconds = err.retryAfterSeconds;
|
|
196
|
-
} else if (err instanceof CallBudgetExhausted) {
|
|
197
|
-
truncatedReason = "max-api-calls";
|
|
198
|
-
} else {
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const summary = {
|
|
204
|
-
ready: releasePrs.filter((p) => p.status === "ready").length,
|
|
205
|
-
needsChangelog: releasePrs.filter((p) => p.status === "needs-changelog").length,
|
|
206
|
-
unknownVersion: releasePrs.filter((p) => p.status === "unknown-version").length
|
|
207
|
-
};
|
|
208
|
-
let verdicts: Partial<Record<ReleaseVerdict, number>> | undefined;
|
|
209
|
-
if (input.analyze) {
|
|
210
|
-
verdicts = {};
|
|
211
|
-
for (const p of releasePrs) if (p.analysis) verdicts[p.analysis.verdict] = (verdicts[p.analysis.verdict] ?? 0) + 1;
|
|
212
|
-
}
|
|
213
|
-
// With analysis, "missing" means "not done": a ready-but-stale changelog still needs work.
|
|
214
|
-
const listed = input.onlyMissing ? releasePrs.filter((p) => (p.analysis ? p.analysis.verdict !== "done" : p.status !== "ready")) : releasePrs;
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
scanned: { releasePrs: hits },
|
|
218
|
-
summary,
|
|
219
|
-
...(verdicts ? { verdicts } : {}),
|
|
220
|
-
releasePrs: listed,
|
|
221
|
-
...(truncatedReason ? { truncated: true, truncatedReason } : {}),
|
|
222
|
-
...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {})
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** JSON Schema advertised to the agent for report_release_prs. */
|
|
227
|
-
export const REPORT_RELEASE_PRS_SCHEMA = {
|
|
228
|
-
type: "object" as const,
|
|
229
|
-
required: ["owner"],
|
|
230
|
-
additionalProperties: false,
|
|
231
|
-
properties: {
|
|
232
|
-
owner: { type: "string", description: "repository owner (org/user login) — selects the read credential" },
|
|
233
|
-
repo: { type: "string", description: "repository name; omit to search every repo this owner has" },
|
|
234
|
-
onlyMissing: { type: "boolean", description: "return only release PRs that still need work (no changelog, unreadable version, or — with analyze — a verdict other than done); summary counts still cover all", default: false },
|
|
235
|
-
analyze: { type: "boolean", description: "also compute, per release PR: open PRs against its release branch (doc / dependency / feature / draft), whether an existing changelog is up to date (missing PRs, version heading, README), and a verdict (done / in-flight / wait / gray / write / update / inspect). ~5 extra calls per release PR — raise maxApiCalls for a whole org", default: false },
|
|
236
|
-
maxApiCalls: { type: "number", description: `hard ceiling on total REST calls this invocation makes (default ${DEFAULT_MAX_API_CALLS}, hard cap ${HARD_MAX_API_CALLS})` }
|
|
237
|
-
}
|
|
238
|
-
};
|