Skip to content

Commit 1018c23

Browse files
committed
pagination in determine-job-url
1 parent 5f8a0a0 commit 1018c23

2 files changed

Lines changed: 152 additions & 46 deletions

File tree

.github/workflows/benchmark-reusable.yml

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -74,39 +74,9 @@ jobs:
7474
BENCHMARK_EVAL: ${{ matrix.eval }}
7575
run: |
7676
set -euo pipefail
77-
# When using reusable workflows, job names get prefixed, so we search for jobs containing our pattern
7877
job_pattern="Benchmark ${BENCHMARK_AGENT} / ${BENCHMARK_MODEL} / ${BENCHMARK_EVAL}"
79-
jobs_endpoint="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100"
80-
job_json="$(curl -fsSL \
81-
-H "Authorization: token ${GITHUB_TOKEN}" \
82-
-H "Accept: application/vnd.github+json" \
83-
"${jobs_endpoint}")"
84-
85-
# Try to find job by exact name match first, then by pattern match
86-
job_info="$(printf '%s\n' "${job_json}" \
87-
| jq -r --arg pattern "$job_pattern" 'select(type=="object" and has("jobs")) | .jobs[] | select(.name | contains($pattern)) | select(.status == "in_progress") | [.id, .html_url] | @tsv' \
88-
| head -n 1)"
89-
90-
if [ -z "${job_info}" ] || [ "${job_info}" = "null" ]; then
91-
echo "Failed to determine job info for pattern: ${job_pattern}" >&2
92-
echo "Available jobs:" >&2
93-
printf '%s\n' "${job_json}" | jq -r '.jobs[]?.name' >&2 || true
94-
exit 1
95-
fi
96-
97-
IFS=$'\t' read -r job_id job_url <<<"${job_info}"
98-
99-
if [ -z "${job_id}" ] || [ "${job_id}" = "null" ]; then
100-
echo "Failed to determine job ID for pattern: ${job_pattern}" >&2
101-
exit 1
102-
fi
78+
job_url="$(bun run scripts/determine-job-url.ts --pattern "${job_pattern}")"
10379
104-
if [ -z "${job_url}" ] || [ "${job_url}" = "null" ]; then
105-
echo "Failed to determine job URL for pattern: ${job_pattern}" >&2
106-
exit 1
107-
fi
108-
109-
echo "Job ID: ${job_id}"
11080
echo "Job URL: ${job_url}"
11181
echo "GITHUB_BENCHMARK_JOB_URL=${job_url}" >> "$GITHUB_ENV"
11282
echo "url=${job_url}" >> "$GITHUB_OUTPUT"
@@ -256,21 +226,7 @@ jobs:
256226
run: |
257227
set -euo pipefail
258228
job_pattern="Judge Analysis - ${MATRIX_EVAL}"
259-
jobs_endpoint="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100"
260-
job_json="$(curl -fsSL \
261-
-H "Authorization: token ${GITHUB_TOKEN}" \
262-
-H "Accept: application/vnd.github+json" \
263-
"${jobs_endpoint}")"
264-
265-
job_url="$(printf '%s\n' "${job_json}" \
266-
| jq -r --arg pattern "$job_pattern" 'select(type=="object" and has("jobs")) | .jobs[] | select(.name | contains($pattern)) | .html_url' \
267-
| head -n 1)"
268-
269-
if [ -z "${job_url}" ] || [ "${job_url}" = "null" ]; then
270-
echo "Failed to determine job URL for pattern ${job_pattern}" >&2
271-
printf '%s\n' "${job_json}" | jq -r '.jobs[]?.name' >&2 || true
272-
exit 1
273-
fi
229+
job_url="$(bun run scripts/determine-job-url.ts --pattern "${job_pattern}")"
274230
275231
step_url="${job_url}#step:7:0"
276232

scripts/determine-job-url.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Determines the HTML URL for a GitHub Actions job within the current run.
4+
*
5+
* Usage:
6+
* bun run scripts/determine-job-url.ts --pattern "Benchmark agent / model / eval"
7+
*/
8+
9+
import process from "node:process";
10+
11+
import { request as octokitRequest } from "@octokit/request";
12+
import type { Endpoints } from "@octokit/types";
13+
14+
type ListWorkflowJobsResponse =
15+
Endpoints["GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs"]["response"]["data"];
16+
type WorkflowJob = NonNullable<ListWorkflowJobsResponse["jobs"]>[number];
17+
18+
function usage(): void {
19+
console.error(
20+
"Usage: bun run scripts/determine-job-url.ts --pattern \"<job name substring>\"",
21+
);
22+
console.error("");
23+
console.error("Looks up the job URL from the current workflow run.");
24+
}
25+
26+
function parseArgs(argv: string[]): { pattern: string } {
27+
let pattern: string | undefined;
28+
29+
for (let index = 0; index < argv.length; index += 1) {
30+
const arg = argv[index];
31+
if (arg === "--pattern") {
32+
pattern = argv[index + 1];
33+
index += 1;
34+
} else {
35+
console.error(`Unknown argument: ${arg}`);
36+
usage();
37+
process.exit(1);
38+
}
39+
}
40+
41+
if (!pattern) {
42+
usage();
43+
process.exit(1);
44+
}
45+
46+
return { pattern };
47+
}
48+
49+
async function fetchJobs(
50+
owner: string,
51+
repo: string,
52+
runId: number,
53+
): Promise<WorkflowJob[]> {
54+
const token = process.env.GITHUB_TOKEN?.trim();
55+
if (!token) {
56+
throw new Error("GITHUB_TOKEN is required to call the GitHub API.");
57+
}
58+
59+
const request = octokitRequest.defaults({
60+
headers: {
61+
authorization: `Bearer ${token}`,
62+
"user-agent": "opencode-bench/job-url",
63+
},
64+
});
65+
66+
const jobs: WorkflowJob[] = [];
67+
const perPage = 100;
68+
let page = 1;
69+
70+
// GitHub caps pagination at 100 items per page. Loop until a page returns fewer rows.
71+
while (true) {
72+
const response = await request(
73+
"GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs",
74+
{
75+
owner,
76+
repo,
77+
run_id: runId,
78+
per_page: perPage,
79+
page,
80+
},
81+
);
82+
83+
const data = response.data as ListWorkflowJobsResponse;
84+
const batch = data.jobs ?? [];
85+
jobs.push(...batch);
86+
87+
if (batch.length < perPage) {
88+
break;
89+
}
90+
91+
page += 1;
92+
}
93+
94+
return jobs;
95+
}
96+
97+
async function main(): Promise<void> {
98+
const repoSlug = process.env.GITHUB_REPOSITORY;
99+
const runIdRaw = process.env.GITHUB_RUN_ID;
100+
101+
if (!repoSlug || !runIdRaw) {
102+
throw new Error(
103+
"GITHUB_REPOSITORY and GITHUB_RUN_ID must be defined in the environment.",
104+
);
105+
}
106+
107+
const runId = Number(runIdRaw);
108+
if (!Number.isFinite(runId)) {
109+
throw new Error(`Invalid GITHUB_RUN_ID value: ${runIdRaw}`);
110+
}
111+
112+
const [owner, repo] = repoSlug.split("/", 2);
113+
if (!owner || !repo) {
114+
throw new Error(`Invalid GITHUB_REPOSITORY value: ${repoSlug}`);
115+
}
116+
117+
const { pattern } = parseArgs(process.argv.slice(2));
118+
const jobs = await fetchJobs(owner, repo, runId);
119+
120+
if (jobs.length === 0) {
121+
throw new Error("No jobs were returned for the current workflow run.");
122+
}
123+
124+
const match = jobs.find((job) => job.name?.includes(pattern));
125+
126+
if (!match) {
127+
console.error(
128+
`Failed to find a job whose name contains "${pattern}". Available jobs:`,
129+
);
130+
for (const job of jobs) {
131+
console.error(`- ${job.name} [status=${job.status}]`);
132+
}
133+
process.exit(1);
134+
}
135+
136+
if (!match.html_url) {
137+
throw new Error(`Job ${match.id} is missing an html_url field.`);
138+
}
139+
140+
process.stdout.write(`${match.html_url}\n`);
141+
}
142+
143+
if (import.meta.main) {
144+
main().catch((error) => {
145+
console.error(
146+
error instanceof Error ? error.message : String(error),
147+
);
148+
process.exit(1);
149+
});
150+
}

0 commit comments

Comments
 (0)