-
-
Notifications
You must be signed in to change notification settings - Fork 461
feat: support nightly spec tests #9221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c0c0782
c2f06a1
5a32bd3
a80e980
804c3fd
a69ecdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,27 @@ | ||
| import path from "node:path"; | ||
| import {config} from "dotenv"; | ||
| import {downloadNightlyTests} from "@lodestar/spec-test-util/downloadNightlyTests"; | ||
| import {downloadTests} from "@lodestar/spec-test-util/downloadTests"; | ||
| import {blsSpecTests, ethereumConsensusSpecsTests} from "./specTestVersioning.js"; | ||
|
|
||
| for (const downloadTestOpts of [ethereumConsensusSpecsTests, blsSpecTests]) { | ||
| downloadTests(downloadTestOpts, console.log).catch((e: Error) => { | ||
| console.error(e); | ||
| process.exit(1); | ||
| }); | ||
| const [date, repo, branch] = process.argv.slice(2); | ||
| const downloads = [downloadTests(blsSpecTests, console.log)]; | ||
|
|
||
| if (date) { | ||
| config({path: path.join(import.meta.dirname, "../../../../.env")}); | ||
|
|
||
| const opts = { | ||
| ...ethereumConsensusSpecsTests, | ||
| ...(repo && {specTestsRepoUrl: `https://github.com/${repo}`}), | ||
| ...(branch && {branch}), | ||
| }; | ||
|
|
||
| downloads.push(downloadNightlyTests(opts, console.log, date)); | ||
| } else { | ||
| downloads.push(downloadTests(ethereumConsensusSpecsTests, console.log)); | ||
| } | ||
|
|
||
| await Promise.all(downloads).catch((e: Error) => { | ||
| console.error(e); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import {fetch} from "@lodestar/utils"; | ||
| import {downloadGenericSpecTests} from "./downloadTests.js"; | ||
|
|
||
| type WorkflowRunsResponse = {workflow_runs: {id: number}[]}; | ||
| type ArtifactsListResponse = {artifacts: {archive_download_url: string; expired: boolean; name: string}[]}; | ||
|
|
||
| async function ghApiFetch<T>(endpoint: string, token: string): Promise<T> { | ||
| const res = await fetch(`https://api.github.com${endpoint}`, { | ||
| headers: {Authorization: `token ${token}`, Accept: "application/vnd.github+json"}, | ||
| signal: AbortSignal.timeout(30_000), | ||
| }); | ||
|
|
||
| if (!res.ok) { | ||
| throw new Error( | ||
| res.status === 401 ? "GITHUB_TOKEN is invalid or expired" : `GitHub API ${res.status} (${endpoint})` | ||
| ); | ||
| } | ||
|
|
||
| return res.json() as Promise<T>; | ||
| } | ||
|
|
||
| async function resolveNightlyRunId(repo: string, token: string, date?: string, branch?: string): Promise<number> { | ||
| const params = new URLSearchParams({status: "success", per_page: "1"}); | ||
| if (branch) params.append("branch", branch); | ||
| if (date) params.append("created", date); | ||
|
|
||
| const {workflow_runs} = await ghApiFetch<WorkflowRunsResponse>( | ||
| `/repos/${repo}/actions/workflows/tests.yml/runs?${params}`, | ||
| token | ||
| ); | ||
|
|
||
| const runId = workflow_runs[0]?.id; | ||
| if (!runId) { | ||
| throw new Error(`No successful run found${date ? ` on ${date}` : ""} for ${repo}${branch ? ` (${branch})` : ""}`); | ||
| } | ||
| return runId; | ||
| } | ||
|
|
||
| export async function downloadNightlyTests( | ||
| opts: {specTestsRepoUrl: string; outputDir: string; testsToDownload: string[]; branch?: string}, | ||
| log: (msg: string) => void, | ||
| date?: string | ||
| ): Promise<void> { | ||
| const token = process.env.GITHUB_TOKEN; | ||
| if (!token) throw new Error("GITHUB_TOKEN is required for nightly downloads"); | ||
|
|
||
| const resolvedDate = date === "latest" || !date ? undefined : date; | ||
| if (resolvedDate && !/^\d{4}-\d{2}-\d{2}$/.test(resolvedDate)) { | ||
| throw new Error(`Invalid date: "${date}". Expected "latest" or YYYY-MM-DD`); | ||
| } | ||
|
|
||
| const repo = new URL(opts.specTestsRepoUrl).pathname.slice(1).replace(/\/$/, ""); | ||
| const runId = await resolveNightlyRunId(repo, token, resolvedDate, opts.branch); | ||
| log(`Resolved nightly${resolvedDate ? ` ${resolvedDate}` : ""} to run ${runId}`); | ||
|
|
||
| const {artifacts} = await ghApiFetch<ArtifactsListResponse>(`/repos/${repo}/actions/runs/${runId}/artifacts`, token); | ||
|
|
||
| const urlByTest: Record<string, string> = {}; | ||
| const available: string[] = []; | ||
| for (const test of opts.testsToDownload) { | ||
| const artifact = artifacts.find((a) => a.name === `${test}.tar.gz` && !a.expired); | ||
| if (artifact) { | ||
| urlByTest[test] = artifact.archive_download_url; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is incorrect. The |
||
| available.push(test); | ||
| } else { | ||
| log(`Skipping ${test} (not found in run ${runId})`); | ||
| } | ||
| } | ||
|
|
||
| if (available.length === 0) throw new Error(`No matching artifacts found in run ${runId}`); | ||
|
Comment on lines
+66
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This code only errors when zero artifacts are found, but it silently proceeds when a subset is missing (for example, a workflow run that did not upload Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is expected behavior |
||
|
|
||
| const authInit: RequestInit = {headers: {Authorization: `token ${token}`, Accept: "application/vnd.github+json"}}; | ||
|
|
||
| await downloadGenericSpecTests( | ||
| { | ||
| specVersion: `nightly-${runId}`, | ||
| specTestsRepoUrl: opts.specTestsRepoUrl, | ||
| outputDir: opts.outputDir, | ||
| testsToDownload: available, | ||
| testUrls: urlByTest, | ||
| fetchInit: authInit, | ||
| }, | ||
| log | ||
| ); | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to see, maybe these need to be moved under
blockson the spec side but this works for nowThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed — if consensus-specs ends up categorizing these under
blocks/pyspec_tests/(either by renaming the file or by routing epoch_boundary through the existingblockshandler in the generator), the Lodestar side stays a no-op: thecase "epoch_boundary":fallthrough is harmless if the directory disappears, and can be dropped later. Either way this doesn't block #9221.