refactor: split github cli implementation (#908)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split
|
||||
// Moved mechanically from scripts/src/gh.ts:7474-7605.
|
||||
|
||||
import { repoParts } from "./auth-and-safety";
|
||||
import { commandError, githubRequest, isGitHubError, runnerDisposition } from "./client";
|
||||
import { issueListPaginationSummary, listIssues } from "./issue-read";
|
||||
import { issueLifecycleBatchSummary } from "./issue-summary";
|
||||
import { MAX_ISSUE_LIST_LIMIT } from "./types";
|
||||
import type { GitHubCommandResult, GitHubDegradedReason, GitHubErrorPayload, GitHubIssue, GitHubOptions, RunnerDisposition } from "./types";
|
||||
|
||||
export function parseGitHubTimestamp(value: string | undefined): number | null {
|
||||
if (value === undefined) return null;
|
||||
const millis = Date.parse(value);
|
||||
return Number.isFinite(millis) ? millis : null;
|
||||
}
|
||||
|
||||
export function inactiveIssueCandidates(issues: GitHubIssue[], cutoffMs: number): GitHubIssue[] {
|
||||
return issues.filter((issue) => {
|
||||
const updatedAtMs = parseGitHubTimestamp(issue.updated_at);
|
||||
return updatedAtMs !== null && updatedAtMs < cutoffMs;
|
||||
});
|
||||
}
|
||||
|
||||
export async function closeIssueForBatch(repo: string, token: string, issueNumber: number): Promise<GitHubIssue | GitHubErrorPayload> {
|
||||
const { owner, name } = repoParts(repo);
|
||||
return githubRequest<GitHubIssue>(token, "PATCH", `/repos/${owner}/${name}/issues/${issueNumber}`, { state: "closed" });
|
||||
}
|
||||
|
||||
export async function issueStaleClose(repo: string, token: string, options: GitHubOptions): Promise<GitHubCommandResult> {
|
||||
const command = "issue stale-close";
|
||||
const observedAt = new Date();
|
||||
const cutoffMs = observedAt.getTime() - Math.round(options.inactiveHours * 60 * 60 * 1000);
|
||||
const cutoffAt = new Date(cutoffMs).toISOString();
|
||||
const result = await listIssues(token, repo, "open", options.limit, "", options.labels);
|
||||
if (isGitHubError(result)) {
|
||||
return commandError(command, repo, result, {
|
||||
state: "open",
|
||||
limit: options.limit,
|
||||
inactiveHours: options.inactiveHours,
|
||||
cutoffAt,
|
||||
labels: options.labels,
|
||||
});
|
||||
}
|
||||
const staleIssues = inactiveIssueCandidates(result.items, cutoffMs);
|
||||
const base = {
|
||||
ok: true,
|
||||
command,
|
||||
repo,
|
||||
dryRun: options.dryRun,
|
||||
state: "open",
|
||||
inactiveHours: options.inactiveHours,
|
||||
observedAt: observedAt.toISOString(),
|
||||
cutoffAt,
|
||||
limit: options.limit,
|
||||
labels: options.labels,
|
||||
scannedCount: result.items.length,
|
||||
rawCount: result.rawCount,
|
||||
staleCount: staleIssues.length,
|
||||
pagination: issueListPaginationSummary(result),
|
||||
hasMore: result.hasMore,
|
||||
stale: issueLifecycleBatchSummary(staleIssues),
|
||||
policy: {
|
||||
basis: "GitHub issue updatedAt",
|
||||
selectedWhen: "updatedAt is older than observedAt - inactiveHours",
|
||||
commentsAndStateChangesCountAsActivity: true,
|
||||
pullRequestsFiltered: true,
|
||||
},
|
||||
readCommands: {
|
||||
dryRun: `bun scripts/cli.ts gh issue stale-close --repo ${repo} --inactive-hours ${options.inactiveHours} --limit ${options.limit} --dry-run`,
|
||||
openList: `bun scripts/cli.ts gh issue list --repo ${repo} --state open --limit ${options.limit} --json number,title,state,url,updatedAt`,
|
||||
},
|
||||
...(result.hasMore && options.limit < MAX_ISSUE_LIST_LIMIT
|
||||
? {
|
||||
next: {
|
||||
command: `bun scripts/cli.ts gh issue stale-close --repo ${repo} --inactive-hours ${options.inactiveHours} --limit ${Math.min(options.limit * 2, MAX_ISSUE_LIST_LIMIT)} --dry-run`,
|
||||
note: "The scan reached the bounded --limit before GitHub pagination was exhausted; increase --limit before treating the cleanup as complete.",
|
||||
},
|
||||
}
|
||||
: result.hasMore
|
||||
? {
|
||||
scanLimit: {
|
||||
maxLimitReached: true,
|
||||
maxLimit: MAX_ISSUE_LIST_LIMIT,
|
||||
note: "The cleanup reached the maximum bounded issue scan; narrow with --label or split the policy before treating it as complete.",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
...base,
|
||||
planned: true,
|
||||
wouldCloseCount: staleIssues.length,
|
||||
wouldCloseNumbers: staleIssues.map((issue) => issue.number),
|
||||
note: staleIssues.length === 0
|
||||
? "No open issues matched the inactive-hours policy; no GitHub issue would be closed."
|
||||
: "Dry-run only; no GitHub issue was closed.",
|
||||
};
|
||||
}
|
||||
|
||||
const closed: GitHubIssue[] = [];
|
||||
const failures: Array<Record<string, unknown>> = [];
|
||||
for (const issue of staleIssues) {
|
||||
const closedIssue = await closeIssueForBatch(repo, token, issue.number);
|
||||
if (isGitHubError(closedIssue)) {
|
||||
failures.push({
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
url: issue.html_url,
|
||||
updatedAt: issue.updated_at ?? null,
|
||||
degradedReason: closedIssue.degradedReason,
|
||||
runnerDisposition: closedIssue.runnerDisposition,
|
||||
message: closedIssue.message,
|
||||
status: closedIssue.status ?? null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
closed.push(closedIssue);
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
dryRun: false,
|
||||
planned: false,
|
||||
closedCount: closed.length,
|
||||
failedCount: failures.length,
|
||||
closed: issueLifecycleBatchSummary(closed),
|
||||
failures,
|
||||
rest: true,
|
||||
ok: failures.length === 0,
|
||||
...(failures.length > 0
|
||||
? {
|
||||
degradedReason: "github-transient" as GitHubDegradedReason,
|
||||
runnerDisposition: "infra-blocked" as RunnerDisposition,
|
||||
retryable: true,
|
||||
}
|
||||
: {}),
|
||||
note: failures.length === 0
|
||||
? "Closed all open issues that matched the inactive-hours policy."
|
||||
: "Some stale issue close operations failed; rerun the same command after checking failures.",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user