|
| 1 | +import {fetch} from "@lodestar/utils"; |
| 2 | +import {downloadGenericSpecTests} from "./downloadTests.js"; |
| 3 | + |
| 4 | +type WorkflowRunsResponse = {workflow_runs: {id: number}[]}; |
| 5 | +type ArtifactsListResponse = {artifacts: {archive_download_url: string; expired: boolean; name: string}[]}; |
| 6 | + |
| 7 | +async function ghApiFetch<T>(endpoint: string, token: string): Promise<T> { |
| 8 | + const res = await fetch(`https://api.github.com${endpoint}`, { |
| 9 | + headers: {Authorization: `token ${token}`, Accept: "application/vnd.github+json"}, |
| 10 | + signal: AbortSignal.timeout(30_000), |
| 11 | + }); |
| 12 | + |
| 13 | + if (!res.ok) { |
| 14 | + throw new Error( |
| 15 | + res.status === 401 ? "GITHUB_TOKEN is invalid or expired" : `GitHub API ${res.status} (${endpoint})` |
| 16 | + ); |
| 17 | + } |
| 18 | + |
| 19 | + return res.json() as Promise<T>; |
| 20 | +} |
| 21 | + |
| 22 | +async function resolveNightlyRunId(repo: string, token: string, date?: string, branch?: string): Promise<number> { |
| 23 | + const params = new URLSearchParams({status: "success", per_page: "1"}); |
| 24 | + if (branch) params.append("branch", branch); |
| 25 | + if (date) params.append("created", date); |
| 26 | + |
| 27 | + const {workflow_runs} = await ghApiFetch<WorkflowRunsResponse>( |
| 28 | + `/repos/${repo}/actions/workflows/tests.yml/runs?${params}`, |
| 29 | + token |
| 30 | + ); |
| 31 | + |
| 32 | + const runId = workflow_runs[0]?.id; |
| 33 | + if (!runId) { |
| 34 | + throw new Error(`No successful run found${date ? ` on ${date}` : ""} for ${repo}${branch ? ` (${branch})` : ""}`); |
| 35 | + } |
| 36 | + return runId; |
| 37 | +} |
| 38 | + |
| 39 | +export async function downloadNightlyTests( |
| 40 | + opts: {specTestsRepoUrl: string; outputDir: string; testsToDownload: string[]; branch?: string}, |
| 41 | + log: (msg: string) => void, |
| 42 | + date?: string |
| 43 | +): Promise<void> { |
| 44 | + const token = process.env.GITHUB_TOKEN; |
| 45 | + if (!token) throw new Error("GITHUB_TOKEN is required for nightly downloads"); |
| 46 | + |
| 47 | + const resolvedDate = date === "latest" || !date ? undefined : date; |
| 48 | + if (resolvedDate && !/^\d{4}-\d{2}-\d{2}$/.test(resolvedDate)) { |
| 49 | + throw new Error(`Invalid date: "${date}". Expected "latest" or YYYY-MM-DD`); |
| 50 | + } |
| 51 | + |
| 52 | + const repo = new URL(opts.specTestsRepoUrl).pathname.slice(1).replace(/\/$/, ""); |
| 53 | + const runId = await resolveNightlyRunId(repo, token, resolvedDate, opts.branch); |
| 54 | + log(`Resolved nightly${resolvedDate ? ` ${resolvedDate}` : ""} to run ${runId}`); |
| 55 | + |
| 56 | + const {artifacts} = await ghApiFetch<ArtifactsListResponse>(`/repos/${repo}/actions/runs/${runId}/artifacts`, token); |
| 57 | + |
| 58 | + const urlByTest: Record<string, string> = {}; |
| 59 | + const available: string[] = []; |
| 60 | + for (const test of opts.testsToDownload) { |
| 61 | + const artifact = artifacts.find((a) => a.name === `${test}.tar.gz` && !a.expired); |
| 62 | + if (artifact) { |
| 63 | + urlByTest[test] = artifact.archive_download_url; |
| 64 | + available.push(test); |
| 65 | + } else { |
| 66 | + log(`Skipping ${test} (not found in run ${runId})`); |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + if (available.length === 0) throw new Error(`No matching artifacts found in run ${runId}`); |
| 71 | + |
| 72 | + const authInit: RequestInit = {headers: {Authorization: `token ${token}`, Accept: "application/vnd.github+json"}}; |
| 73 | + |
| 74 | + await downloadGenericSpecTests( |
| 75 | + { |
| 76 | + specVersion: `nightly-${runId}`, |
| 77 | + specTestsRepoUrl: opts.specTestsRepoUrl, |
| 78 | + outputDir: opts.outputDir, |
| 79 | + testsToDownload: available, |
| 80 | + testUrls: urlByTest, |
| 81 | + fetchInit: authInit, |
| 82 | + }, |
| 83 | + log |
| 84 | + ); |
| 85 | +} |
0 commit comments