feat: read GitHub Pages benchmark exports

This commit is contained in:
pikastech
2026-07-18 06:15:52 +02:00
parent d4efa67141
commit a3e5676abe
10 changed files with 388 additions and 6 deletions
+9
View File
@@ -57,6 +57,7 @@ bun scripts/cli.ts gh pr list --repo pikasTech/unidesk --state all --limit 10
bun scripts/cli.ts gh run list --repo owner/name --limit 10
bun scripts/cli.ts gh run view <run-id> --repo owner/name
bun scripts/cli.ts gh run logs <run-id> --job <job-id> --repo owner/name
bun scripts/cli.ts gh run benchmark --repo owner/name --url https://owner.github.io/repo/dev/bench/ --limit 10
trans gh:/pikasTech/unidesk/pr/<number> cat
trans gh:/pikasTech/unidesk/pr/<number> rg <pattern>
bun scripts/cli.ts gh pr review-plan <number> --repo pikasTech/unidesk
@@ -67,6 +68,14 @@ bun scripts/cli.ts gh pr preflight <number> --repo pikasTech/unidesk
bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk --merge --delete-branch
```
- `gh run benchmark` 读取 `github-action-benchmark` 发布到 GitHub Pages
`data.js`
- `--url` 可以传页面目录或 `data.js`
- 默认输出有界历史和最新测量;
- `--commit <sha-prefix> --raw` 返回筛选后的结构化
`entries``measurements`
- 不解析 job log,也不默认输出完整导出文件。
机器结构化入口必须显式请求:
```bash
@@ -38,6 +38,8 @@ GitHub入口负责为 UniDesk/HWLAB 执行过程提供受控 GitHub issue、PR
### 2.2 范围内
- `bun scripts/cli.ts gh ...` 的 GitHub issue/PR/list/read/write/comment/close/reopen/merge/preflight 等受控命令。
- GitHub Actions run、job、日志诊断和 `github-action-benchmark`
Pages 导出物的有界只读查询。
- `trans gh:/owner/repo/...` 虚拟文件写回所依赖的 issue/PR body read、body patch、sha guard 和 update guard。
- GitHub REST/GraphQL token resolution、request、错误分类、rate/repo/issue 探测和脱敏输出。
- issue/PR 正文 stdin/file/patch 输入、短 body safety、profile guard、body sha、updatedAt 和 comment concurrency guard。
@@ -63,15 +65,16 @@ GitHub入口负责为 UniDesk/HWLAB 执行过程提供受控 GitHub issue、PR
| `trans gh:` | 把 GitHub issue/PR 正文映射为虚拟文件的 route,最终仍调用 UniDesk GitHub入口写回。 |
| scoped help | `gh issue --help``gh pr merge --help` 这类按命令前缀过滤的有界帮助输出。 |
| bounded dump | CLI 输出超过全局阈值时写入临时 dump,并在 stdout 返回摘要和读取命令。 |
| benchmark 导出物 | `github-action-benchmark` 发布到 GitHub Pages 的 `data.js` 历史数据;它是 Action 产出的只读性能记录,不等同于 job 日志。 |
## 4. 系统边界和接口
| 边界项 | 内容 |
| --- | --- |
| 外部使用者 | 维护者、agent、项目治理流程、PR closeout 流程和 `trans gh:` 路由。 |
| 外部输入 | GitHub token 来源、owner/repo、issue/PR number、正文、patch、commentId、board row、attachment 选择、merge method 和输出模式。 |
| 外部输入 | GitHub token 来源、owner/repo、issue/PR number、正文、patch、commentId、board row、attachment 选择、merge method、Actions run/job、benchmark Pages URL 和输出模式。 |
| 受控资源 | `scripts/src/gh.ts` 兼容入口、`scripts/src/gh/` 子模块、GitHub REST/GraphQL request、body guard、merge guard、board mutation plan、attachment 下载和 help/dispatch。 |
| 外部输出 | GitHub 资源摘要、受控写入结果、body sha、comment id、PR preflight、merge 状态、bounded dump、错误分类和后续 drill-down 命令。 |
| 外部输出 | GitHub 资源摘要、受控写入结果、body sha、comment id、PR preflight、merge 状态、Actions 状态、benchmark 历史摘要、bounded dump、错误分类和后续 drill-down 命令。 |
| 用户接口 | `bun scripts/cli.ts gh ...``bun scripts/cli.ts help` 中的 gh 入口、`trans gh:/owner/repo/...`。 |
| 系统边界 | GitHub入口负责 GitHub 读写路径、保护策略、输出契约和模块边界;不拥有业务 issue 的需求内容、Git source truth 或发布判定。 |
@@ -164,7 +167,9 @@ GitHub入口应保持 `bun scripts/cli.ts gh ...`、`ghHelp`、`ghScopedHelp`、
| --- | --- | --- | --- |
| OPS-GH-REQ-002 | 请求选项 | PJ2026-01060602 请求与选项 | [源码同步](PJ2026-010602-source-sync.md) |
GitHub入口应集中处理 token resolution、REST/GraphQL request、错误分类、owner/repo、issue/PR number、URL/shorthand、JSON fields、known option validation 和 read/view target resolution。认证失败、repo 不存在、权限不足、GitHub transient 和 unsupported command 必须结构化输出,不得静默 fallback 到原生 `gh`、raw `curl` 或裸 API 调用。
GitHub入口应集中处理 token resolution、REST/GraphQL request、公开 GitHub Pages 读取、错误分类、owner/repo、issue/PR number、URL/shorthand、JSON fields、known option validation 和 read/view target resolution。认证失败、repo 不存在、权限不足、GitHub transient、Pages 导出无效和 unsupported command 必须结构化输出,不得静默 fallback 到原生 `gh`、raw `curl` 或裸 API 调用。
GitHub Pages 读取只接受 HTTPS URL,拒绝 URL credential,并对响应体设置有界字节预算。`github-action-benchmark` 页面 URL 可以解析到同目录 `data.js`;CLI 必须验证导出 schema,再按 limit 或 commit 选择记录,不能执行导出文件中的 JavaScript。
### 6.3 OPS-GH-REQ-003 正文保护与并发 guard
@@ -198,6 +203,8 @@ GitHub入口应支持 board table parse、board-row list/get/update/add/upsert/m
GitHub入口应保持低噪声默认输出、稳定 `--json` 字段选择、`--raw``--full`、bounded dump 和 scoped help 行为。`gh --help``gh issue --help``gh pr --help` 和子命令 help 必须快速返回有界 JSON,不因拆分丢失 usage、notes 或 next command。
Actions benchmark 默认输出只能展示有界提交摘要和最近记录的测量表;结构化模式返回已经筛选的 entries 和 measurements,不回显完整 `data.js`。结果必须披露 source URL、导出更新时间、总记录数、返回记录数和 mutation=false,使调用方能判断数据新鲜度与裁剪范围。
## 7. 过程控制
本规格的执行 issue 为 [#906](https://github.com/pikasTech/unidesk/issues/906)。后续影响 GitHub入口稳定契约、模块边界、body guard、merge guard 或 `trans gh:` 写回语义的变更,应先更新本规格或在对应 issue 中说明为何不改变规格。
+50
View File
@@ -1,8 +1,34 @@
import { describe, expect, test } from "bun:test";
import { actionLogFailureExcerpt, isActionLogNotReady, redactAndTailActionLog } from "./gh/actions-runs";
import { benchmarkDataUrl, parseBenchmarkData } from "./gh/actions-benchmark";
import { withGhDefaultRendered } from "./gh/default-render";
describe("GitHub Actions run diagnostics", () => {
test("parses github-action-benchmark Pages exports without executing JavaScript", () => {
const parsed = parseBenchmarkData(`window.BENCHMARK_DATA = {
"lastUpdate": 123,
"repoUrl": "https://github.com/owner/repo",
"entries": {
"Benchmark": [{
"commit": {"id": "abcdef", "message": "perf: faster", "timestamp": "2026-01-01T00:00:00Z", "url": "https://github.com/owner/repo/commit/abcdef"},
"date": 123,
"tool": "googlecpp",
"benches": [{"name": "loop", "value": 1.25, "unit": "ms/iter", "extra": "iterations: 10"}]
}]
}
};`);
expect(parsed.entries).toHaveLength(1);
expect(parsed.entries[0]?.benches[0]?.value).toBe(1.25);
expect(parsed.suites).toEqual(["Benchmark"]);
});
test("resolves a benchmark page to data.js and rejects a mismatched Pages owner", () => {
expect(benchmarkDataUrl("owner/repo", "https://owner.github.io/repo/dev/bench/"))
.toBe("https://owner.github.io/repo/dev/bench/data.js");
expect(() => benchmarkDataUrl("owner/repo", "https://other.github.io/repo/dev/bench/"))
.toThrow("host must be owner.github.io");
});
test("redacts credentials and keeps a bounded tail", () => {
const lines = Array.from({ length: 200 }, (_, index) => `line-${index}`);
lines.push("TOKEN=secret-value");
@@ -64,5 +90,29 @@ describe("GitHub Actions run diagnostics", () => {
expect(logs.renderedText).toContain("compile failed at source.c:10");
expect(logs.renderedText).toContain("first failed assertion");
expect(logs.renderedText).toContain("valuesPrinted=false");
const benchmark = withGhDefaultRendered(["run", "benchmark"], {
ok: true,
command: "run benchmark",
repo: "owner/repo",
source: { dataUrl: "https://owner.github.io/repo/dev/bench/data.js", bytes: 1024, lastUpdate: 123 },
suites: ["Benchmark"],
totalEntries: 2,
matchingEntries: 2,
returnedEntries: 1,
entries: [{
commit: { id: "abcdef", message: "perf: faster" },
date: 123,
measurementCount: 2,
}],
measurements: [
{ commit: "abcdef", name: "Performance Points", value: 18000, unit: "Point/iter" },
{ commit: "abcdef", name: "loop", value: 1.25, unit: "ms/iter" },
],
next: { commit: "gh run benchmark --commit <sha>" },
}) as { renderedText: string };
expect(benchmark.renderedText).toContain("18000.00");
expect(benchmark.renderedText).toContain("loop");
expect(benchmark.renderedText).toContain("never the full data.js source");
});
});
+185
View File
@@ -0,0 +1,185 @@
import { commandError, errorPayload, githubPagesTextRequest, isGitHubError, validationError } from "./client";
import { isRecord } from "./notify-claudeqq";
const BENCHMARK_EXPORT_MAX_BYTES = 8 * 1024 * 1024;
interface BenchmarkMeasurement {
name: string;
value: number;
unit: string;
extra: string | null;
}
interface BenchmarkEntry {
suite: string;
commit: {
id: string;
message: string;
timestamp: string | null;
url: string | null;
};
date: number;
tool: string;
benches: BenchmarkMeasurement[];
}
function splitRepo(repo: string): [string, string] | null {
const [owner, name, extra] = repo.split("/");
return owner && name && extra === undefined ? [owner, name] : null;
}
export function benchmarkDataUrl(repo: string, input?: string): string {
const parts = splitRepo(repo);
if (parts === null) throw new Error("--repo must use owner/name format");
const [owner, name] = parts;
const page = new URL(input ?? `https://${owner}.github.io/${name}/dev/bench/`);
if (page.protocol !== "https:" || page.username !== "" || page.password !== "") {
throw new Error("--url must be an HTTPS GitHub Pages URL without embedded credentials");
}
if (page.hostname.toLowerCase() !== `${owner.toLowerCase()}.github.io`) {
throw new Error(`--url host must be ${owner.toLowerCase()}.github.io for repo ${repo}`);
}
const repoPrefix = `/${name.toLowerCase()}/`;
if (!page.pathname.toLowerCase().startsWith(repoPrefix)) {
throw new Error(`--url path must start with /${name}/ for repo ${repo}`);
}
page.search = "";
page.hash = "";
if (page.pathname.endsWith("/data.js")) return page.toString();
if (!page.pathname.endsWith("/")) page.pathname += "/";
return new URL("data.js", page).toString();
}
function text(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function parseMeasurement(value: unknown): BenchmarkMeasurement | null {
if (!isRecord(value)) return null;
const name = text(value.name);
const unit = text(value.unit);
if (name === null || unit === null || typeof value.value !== "number" || !Number.isFinite(value.value)) return null;
return { name, value: value.value, unit, extra: text(value.extra) };
}
function parseEntry(suite: string, value: unknown): BenchmarkEntry | null {
if (!isRecord(value) || !isRecord(value.commit) || !Array.isArray(value.benches)) return null;
const id = text(value.commit.id);
const message = text(value.commit.message);
if (id === null || message === null || typeof value.date !== "number" || !Number.isFinite(value.date)) return null;
const benches = value.benches.map(parseMeasurement).filter((item): item is BenchmarkMeasurement => item !== null);
if (benches.length === 0) return null;
return {
suite,
commit: {
id,
message,
timestamp: text(value.commit.timestamp),
url: text(value.commit.url),
},
date: value.date,
tool: text(value.tool) ?? "unknown",
benches,
};
}
export function parseBenchmarkData(textValue: string): {
lastUpdate: number | null;
repoUrl: string | null;
entries: BenchmarkEntry[];
suites: string[];
} {
const prefix = /^\s*window\.BENCHMARK_DATA\s*=\s*/u;
if (!prefix.test(textValue)) throw new Error("benchmark export must assign window.BENCHMARK_DATA");
const json = textValue.replace(prefix, "").replace(/;?\s*$/u, "");
const parsed: unknown = JSON.parse(json);
if (!isRecord(parsed) || !isRecord(parsed.entries)) throw new Error("benchmark export is missing entries");
const suites = Object.keys(parsed.entries);
const entries = suites.flatMap((suite) => {
const values = parsed.entries[suite];
return Array.isArray(values)
? values.map((value) => parseEntry(suite, value)).filter((entry): entry is BenchmarkEntry => entry !== null)
: [];
}).sort((left, right) => left.date - right.date);
if (entries.length === 0) throw new Error("benchmark export contains no valid entries");
return {
lastUpdate: typeof parsed.lastUpdate === "number" && Number.isFinite(parsed.lastUpdate) ? parsed.lastUpdate : null,
repoUrl: text(parsed.repoUrl),
entries,
suites,
};
}
export async function actionBenchmark(
repo: string,
options: { url?: string; commit?: string; limit: number },
): Promise<Record<string, unknown>> {
let dataUrl: string;
try {
dataUrl = benchmarkDataUrl(repo, options.url);
} catch (error) {
return validationError("run benchmark", repo, error instanceof Error ? error.message : String(error));
}
const fetched = await githubPagesTextRequest(dataUrl, BENCHMARK_EXPORT_MAX_BYTES);
if (isGitHubError(fetched)) return commandError("run benchmark", repo, fetched, { mutation: false, dataUrl });
let parsed: ReturnType<typeof parseBenchmarkData>;
try {
parsed = parseBenchmarkData(fetched.text);
} catch (error) {
return commandError("run benchmark", repo, errorPayload("invalid-response", error instanceof Error ? error.message : String(error), {
request: { method: "GET", url: dataUrl },
details: { bytes: fetched.bytes, contentType: fetched.contentType },
}), { mutation: false, dataUrl });
}
const commit = options.commit?.toLowerCase();
const matching = commit === undefined
? parsed.entries
: parsed.entries.filter((entry) => entry.commit.id.toLowerCase().startsWith(commit));
if (matching.length === 0) {
return validationError("run benchmark", repo, `no benchmark entry matched commit ${options.commit}`, {
dataUrl,
totalEntries: parsed.entries.length,
});
}
const selectedEntries = matching.slice(-options.limit).reverse();
const entries = selectedEntries.map((entry) => ({
suite: entry.suite,
commit: entry.commit,
date: entry.date,
tool: entry.tool,
measurementCount: entry.benches.length,
}));
const measurements = selectedEntries.flatMap((entry) => entry.benches.map((bench) => ({
suite: entry.suite,
commit: entry.commit.id,
date: entry.date,
name: bench.name,
value: bench.value,
unit: bench.unit,
})));
return {
ok: true,
command: "run benchmark",
repo,
mutation: false,
source: {
pageUrl: options.url ?? dataUrl.replace(/data\.js$/u, ""),
dataUrl,
bytes: fetched.bytes,
contentType: fetched.contentType,
lastUpdate: parsed.lastUpdate,
repoUrl: parsed.repoUrl,
},
suites: parsed.suites,
totalEntries: parsed.entries.length,
matchingEntries: matching.length,
returnedEntries: selectedEntries.length,
limit: options.limit,
commitFilter: options.commit ?? null,
entries,
measurements,
next: {
commit: `bun scripts/cli.ts gh run benchmark --repo ${repo} --url ${options.url ?? dataUrl.replace(/data\.js$/u, "")} --commit <sha> --raw`,
},
};
}
+60
View File
@@ -307,6 +307,66 @@ export async function githubTextRequest(
return text;
}
export async function githubPagesTextRequest(
url: string,
byteLimit: number,
): Promise<{ text: string; bytes: number; contentType: string | null } | GitHubErrorPayload> {
let response: Response;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
response = await fetch(url, {
signal: controller.signal,
headers: {
Accept: "text/javascript, application/javascript, text/plain",
"User-Agent": USER_AGENT,
},
});
} catch (error) {
const classified = classifyGitHubFetchFailure(error);
return errorPayload(classified.reason, classified.message, {
request: { method: "GET", url },
details: classified.details,
retryable: classified.retryable,
commanderAction: classified.commanderAction,
});
} finally {
clearTimeout(timeout);
}
if (!response.ok) {
return errorPayload("invalid-response", `GitHub Pages benchmark export returned HTTP ${response.status}`, {
status: response.status,
request: { method: "GET", url },
});
}
const declaredBytes = Number(response.headers.get("content-length"));
if (Number.isFinite(declaredBytes) && declaredBytes > byteLimit) {
return errorPayload("invalid-response", `GitHub Pages benchmark export exceeds ${byteLimit} bytes`, {
request: { method: "GET", url },
details: { declaredBytes, byteLimit },
});
}
const body = await response.arrayBuffer();
if (body.byteLength > byteLimit) {
return errorPayload("invalid-response", `GitHub Pages benchmark export exceeds ${byteLimit} bytes`, {
request: { method: "GET", url },
details: { bytes: body.byteLength, byteLimit },
});
}
try {
return {
text: new TextDecoder("utf-8", { fatal: true }).decode(body),
bytes: body.byteLength,
contentType: response.headers.get("content-type"),
};
} catch {
return errorPayload("invalid-response", "GitHub Pages benchmark export is not valid UTF-8", {
request: { method: "GET", url },
details: { bytes: body.byteLength },
});
}
}
export async function githubGraphqlRequest<T>(
token: string,
query: string,
+50
View File
@@ -53,6 +53,7 @@ function renderGhDefaultText(result: GitHubCommandResult): string {
if (command === "run list") return renderRunList(result);
if (command === "run view") return renderRunView(result);
if (command === "run logs") return renderRunLogs(result);
if (command === "run benchmark") return renderRunBenchmark(result);
if (command === "pr files" || command === "pr diff --stat") return renderPrFiles(result);
if (command === "pr review-plan") return renderPrReviewPlan(result);
if (command === "pr diff" && isRecord(result.file)) return renderPrDiffFile(result);
@@ -755,6 +756,55 @@ function renderRunLogs(result: GitHubCommandResult): string {
].join("\n");
}
function renderRunBenchmark(result: GitHubCommandResult): string {
const entries = arrayOfRecords(result.entries);
const latest = entries[0] ?? {};
const latestCommit = ghText(record(latest.commit).id);
const measurements = arrayOfRecords(result.measurements)
.filter((measurement) => ghText(measurement.commit) === latestCommit);
const historyRows = entries.slice(0, 12).map((entry) => {
const commit = record(entry.commit);
const points = arrayOfRecords(result.measurements)
.find((bench) => bench.commit === commit.id && bench.name === "Performance Points");
return [
ghShort(ghText(commit.id), 10),
benchmarkDate(entry.date),
points === undefined ? "-" : Number(points.value).toFixed(2),
ghShort(ghText(commit.message).split("\n")[0], 72),
];
});
const measurementRows = measurements.slice(0, 30).map((bench) => [
ghShort(ghText(bench.name), 52),
typeof bench.value === "number" ? bench.value.toFixed(6) : ghText(bench.value),
ghText(bench.unit),
]);
const source = record(result.source);
const next = record(result.next);
return [
"gh run benchmark (observed)",
"",
ghTable(["COMMIT", "DATE", "POINTS", "TITLE"], historyRows),
"",
`Latest measurements (${ghShort(ghText(record(latest.commit).id), 10)}):`,
measurementRows.length > 0 ? ghTable(["BENCHMARK", "VALUE", "UNIT"], measurementRows) : "No measurements returned.",
"",
"Summary:",
` repo=${ghText(result.repo)} total=${ghText(result.totalEntries)} matched=${ghText(result.matchingEntries)} returned=${ghText(result.returnedEntries)} suites=${arrayText(result.suites)} mutation=false`,
` source=${ghText(source.dataUrl)} bytes=${ghText(source.bytes)} lastUpdate=${benchmarkDate(source.lastUpdate)}`,
"",
"Next:",
` ${ghText(next.commit)}`,
"",
"Disclosure:",
" default output is bounded to commit history plus the latest measurements; use --raw for selected structured entries, never the full data.js source.",
].join("\n");
}
function benchmarkDate(value: unknown): string {
if (typeof value === "number" && Number.isFinite(value)) return new Date(value).toISOString();
return shortDate(value);
}
function record(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
+4
View File
@@ -47,6 +47,7 @@ export function ghHelp(): unknown {
"bun scripts/cli.ts gh run list --repo owner/name [--limit N]",
"bun scripts/cli.ts gh run view <run-id> --repo owner/name",
"bun scripts/cli.ts gh run logs <run-id> --job <job-id> --repo owner/name",
"bun scripts/cli.ts gh run benchmark --repo owner/name [--url https://owner.github.io/repo/dev/bench/] [--commit sha-prefix] [--limit N] [--raw]",
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--number N compat] [--limit N] [number may appear before or after options]",
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--number N compat] [--limit N] [number may appear before or after options; compatibility alias for pr files; no raw diff]",
"bun scripts/cli.ts gh pr review-plan <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--limit N] [bounded changed-file index plus per-file patch drill-down commands]",
@@ -192,6 +193,9 @@ export function ghScopedHelpNotes(tokens: string[]): string[] {
} else if (key === "pr review-plan" || key === "pr diff") {
notes.push("Use `pr review-plan` first for a bounded changed-file index with per-file drill-down commands.");
notes.push("Use `pr diff <number> --file <path> [--hunk N]` for bounded patch review; full patch disclosure requires explicit --full or --raw.");
} else if (key === "run benchmark") {
notes.push("读取 github-action-benchmark 发布到 GitHub Pages 的 data.js--url 接受页面目录或 data.js URL。");
notes.push("默认只展示有界提交历史和最新一条测量;使用 --commit 缩小到指定提交,使用 --raw 获取筛选后的结构化 entries/measurements。");
}
return notes;
}
+13 -2
View File
@@ -2,6 +2,7 @@
// Moved mechanically from scripts/src/gh.ts:8348-8845.
import { issueAttachmentDownload, issueAttachmentList } from "./attachments";
import { actionBenchmark } from "./actions-benchmark";
import { actionRunList, actionRunLogs, actionRunView } from "./actions-runs";
import { resolveToken } from "./auth-and-safety";
import { authStatus, prFiles, prList, prRead, prView } from "./auth-pr-read";
@@ -115,6 +116,9 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (optionWasProvided(args, "--job") && !(top === "run" && sub === "logs")) {
return validationError([top, sub].filter(Boolean).join(" "), options.repo, "--job is only supported by gh run logs <run-id> --job <job-id>");
}
if ((optionWasProvided(args, "--url") || optionWasProvided(args, "--commit")) && !(top === "run" && sub === "benchmark")) {
return validationError([top, sub].filter(Boolean).join(" "), options.repo, "--url and --commit are only supported by gh run benchmark");
}
if (optionWasProvided(args, "--number") && !allowsNumberTargetAlias(top, sub, third)) {
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
const standardViewCommand = top === "issue" || top === "pr" ? `gh ${top} view` : "gh issue/pr view";
@@ -226,8 +230,15 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (top === "run") {
const commandName = `run ${sub ?? ""}`.trim();
if (sub !== "list" && sub !== "view" && sub !== "logs") {
return unsupportedCommand(commandName, options.repo, "run supported commands are list, view, and logs.");
if (sub !== "list" && sub !== "view" && sub !== "logs" && sub !== "benchmark") {
return unsupportedCommand(commandName, options.repo, "run supported commands are list, view, logs, and benchmark.");
}
if (sub === "benchmark") {
return actionBenchmark(options.repo, {
url: options.benchmarkUrl,
commit: options.benchmarkCommit,
limit: options.limit,
});
}
const { token, probe } = resolveToken(options.repo, true);
const missing = authRequired(options.repo, commandName, probe);
+4
View File
@@ -344,6 +344,8 @@ export function parseOptions(args: string[]): GitHubOptions {
? MAX_ISSUE_LIST_LIMIT
: top === "issue" && sub === "comments"
? DEFAULT_ISSUE_COMMENTS_LIMIT
: top === "run" && sub === "benchmark"
? 10
: 30,
limitMax,
),
@@ -393,5 +395,7 @@ export function parseOptions(args: string[]): GitHubOptions {
outputPath: optionValue(args, "--output"),
filePath: optionValue(args, "--file"),
hunk: optionalPositiveIntegerOption(args, "--hunk"),
benchmarkUrl: optionValue(args, "--url"),
benchmarkCommit: optionValue(args, "--commit")?.trim(),
};
}
+3 -1
View File
@@ -97,7 +97,7 @@ export const GH_VALUE_OPTIONS = new Set([
"--tasks", "--summary", "--focus", "--validation", "--progress", "--number", "--pr",
"--search", "--title-prefix", "--inactive-hours", "--comment", "--comment-file", "--description",
"--attachment", "--output", "--file", "--hunk", "--sync-node",
"--job",
"--job", "--url", "--commit",
]);
export const GH_FLAG_OPTIONS = new Set(["--dry-run", "--draft", "--notify-claudeqq-brief-diff", "--allow-short-body", "--body-stdin", "--body-patch-stdin", "--comment-stdin", "--raw", "--full", "--stat", "--merge", "--squash", "--rebase", "--delete-branch", "--keep-branch", "--skip-local-closeout", "--private", "--public", "--auto-init"]);
@@ -483,6 +483,8 @@ export interface GitHubOptions {
outputPath?: string;
filePath?: string;
hunk?: number;
benchmarkUrl?: string;
benchmarkCommit?: string;
}
export interface GitHubShorthandReference {