mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
9920dbace1
Adds a final step to our e2e deploy test job to upload results for our adapter for comparison. x-ref: https://github.com/vercel/next.js/actions/runs/23471649794/job/68298210825#step:5:40
327 lines
8.3 KiB
JavaScript
327 lines
8.3 KiB
JavaScript
import fs from 'node:fs/promises'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { spawn } from 'node:child_process'
|
|
|
|
const RESULT_FILE_SUFFIX = '.results.json'
|
|
const DEFAULT_ENDPOINT = 'https://nextjs.org/api/adapter-test-results'
|
|
const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i
|
|
const BOOLEAN_ARGS = new Set(['help', 'dry-run'])
|
|
const IGNORED_DIRECTORIES = new Set([
|
|
'.git',
|
|
'.hg',
|
|
'.svn',
|
|
'.next',
|
|
'.turbo',
|
|
'node_modules',
|
|
'dist',
|
|
'out',
|
|
'coverage',
|
|
])
|
|
|
|
function usage() {
|
|
console.log(
|
|
[
|
|
'Collect Jest *.results.json files, zip them, and upload to the adapter test results endpoint.',
|
|
'',
|
|
'Usage:',
|
|
' node scripts/upload-adapter-test-results.mjs \\',
|
|
' --results-root ~/dev/next.js \\',
|
|
' --provider vercel \\',
|
|
' --secret "$ADAPTER_TEST_RESULTS_SECRET" \\',
|
|
' --commit-sha "$(git -C ~/dev/next.js rev-parse HEAD)"',
|
|
'',
|
|
'Required:',
|
|
' --provider <name> or ADAPTER_TEST_RESULTS_PROVIDER',
|
|
' --secret <secret> or ADAPTER_TEST_RESULTS_SECRET',
|
|
' --commit-sha <sha> or ADAPTER_TEST_RESULTS_COMMIT_SHA or GITHUB_SHA',
|
|
'',
|
|
'Optional:',
|
|
` --results-root <path> default: ${process.cwd()}`,
|
|
` --endpoint <url> default: ${DEFAULT_ENDPOINT}`,
|
|
' --zip-out <path> write zip to disk before upload',
|
|
' --dry-run only collect + zip; skip upload',
|
|
' --help show this message',
|
|
].join('\n')
|
|
)
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
/** @type {Record<string, string | boolean>} */
|
|
const args = {}
|
|
|
|
for (let index = 2; index < argv.length; index += 1) {
|
|
const arg = argv[index]
|
|
|
|
if (arg === '--') {
|
|
continue
|
|
}
|
|
|
|
if (!arg.startsWith('--')) {
|
|
throw new Error(`Unexpected argument: ${arg}`)
|
|
}
|
|
|
|
const separatorIndex = arg.indexOf('=')
|
|
if (separatorIndex > -1) {
|
|
const key = arg.slice(2, separatorIndex)
|
|
const value = arg.slice(separatorIndex + 1)
|
|
args[key] = value
|
|
continue
|
|
}
|
|
|
|
const key = arg.slice(2)
|
|
if (BOOLEAN_ARGS.has(key)) {
|
|
args[key] = true
|
|
continue
|
|
}
|
|
|
|
const value = argv[index + 1]
|
|
if (!value || value.startsWith('--')) {
|
|
throw new Error(`Missing value for --${key}`)
|
|
}
|
|
|
|
args[key] = value
|
|
index += 1
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
function resolvePathWithHome(inputPath) {
|
|
if (inputPath === '~') {
|
|
return os.homedir()
|
|
}
|
|
|
|
if (inputPath.startsWith('~/')) {
|
|
return path.join(os.homedir(), inputPath.slice(2))
|
|
}
|
|
|
|
return inputPath
|
|
}
|
|
|
|
function toZipEntryPath(filePath) {
|
|
return filePath.split(path.sep).join('/')
|
|
}
|
|
|
|
async function walkResultFiles(rootDir, currentDir, files) {
|
|
const entries = await fs.readdir(currentDir, { withFileTypes: true })
|
|
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(currentDir, entry.name)
|
|
|
|
if (entry.isDirectory()) {
|
|
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
continue
|
|
}
|
|
|
|
await walkResultFiles(rootDir, entryPath, files)
|
|
continue
|
|
}
|
|
|
|
if (!entry.isFile() || !entry.name.endsWith(RESULT_FILE_SUFFIX)) {
|
|
continue
|
|
}
|
|
|
|
const relativePath = path.relative(rootDir, entryPath)
|
|
files.push({
|
|
absolutePath: entryPath,
|
|
relativePath: toZipEntryPath(relativePath),
|
|
})
|
|
}
|
|
}
|
|
|
|
async function collectResultFiles(resultsRoot) {
|
|
/** @type {{ absolutePath: string; relativePath: string }[]} */
|
|
const files = []
|
|
await walkResultFiles(resultsRoot, resultsRoot, files)
|
|
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))
|
|
return files
|
|
}
|
|
|
|
function formatBytes(value) {
|
|
if (value < 1024) {
|
|
return `${value} B`
|
|
}
|
|
|
|
if (value < 1024 * 1024) {
|
|
return `${(value / 1024).toFixed(1)} KiB`
|
|
}
|
|
|
|
return `${(value / (1024 * 1024)).toFixed(2)} MiB`
|
|
}
|
|
|
|
async function runZipCommand(resultsRoot, files, outputZipPath) {
|
|
const relativePaths = files.map((file) => file.relativePath)
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const child = spawn('zip', ['-q', '-9', outputZipPath, '-@'], {
|
|
cwd: resultsRoot,
|
|
stdio: ['pipe', 'ignore', 'pipe'],
|
|
})
|
|
|
|
let stderr = ''
|
|
|
|
child.on('error', (error) => {
|
|
reject(new Error(`Failed to run zip: ${error.message}`))
|
|
})
|
|
|
|
child.stderr.on('data', (chunk) => {
|
|
stderr += String(chunk)
|
|
})
|
|
|
|
child.on('close', (code, signal) => {
|
|
if (code === 0) {
|
|
resolve(undefined)
|
|
return
|
|
}
|
|
|
|
reject(
|
|
new Error(
|
|
[
|
|
`zip failed with code ${code}${signal ? ` and signal ${signal}` : ''}.`,
|
|
stderr.trim(),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
)
|
|
)
|
|
})
|
|
|
|
child.stdin.write(`${relativePaths.join('\n')}\n`)
|
|
child.stdin.end()
|
|
})
|
|
}
|
|
|
|
async function createArchive(resultsRoot, files) {
|
|
const tempDir = await fs.mkdtemp(
|
|
path.join(os.tmpdir(), 'adapter-test-results-')
|
|
)
|
|
const zipPath = path.join(tempDir, 'results.zip')
|
|
|
|
try {
|
|
await runZipCommand(resultsRoot, files, zipPath)
|
|
return new Uint8Array(await fs.readFile(zipPath))
|
|
} finally {
|
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv)
|
|
|
|
if (args.help) {
|
|
usage()
|
|
return
|
|
}
|
|
|
|
const provider =
|
|
args.provider || process.env.ADAPTER_TEST_RESULTS_PROVIDER || ''
|
|
const secret = args.secret || process.env.ADAPTER_TEST_RESULTS_SECRET || ''
|
|
const commitSha =
|
|
args['commit-sha'] ||
|
|
process.env.ADAPTER_TEST_RESULTS_COMMIT_SHA ||
|
|
process.env.GITHUB_SHA ||
|
|
''
|
|
const endpoint =
|
|
args.endpoint ||
|
|
process.env.ADAPTER_TEST_RESULTS_ENDPOINT ||
|
|
DEFAULT_ENDPOINT
|
|
const inputRoot = args['results-root'] || process.cwd()
|
|
const resultsRoot = path.resolve(resolvePathWithHome(String(inputRoot)))
|
|
const zipOutArg = args['zip-out']
|
|
const zipOutPath = zipOutArg
|
|
? path.resolve(resolvePathWithHome(String(zipOutArg)))
|
|
: null
|
|
const dryRun = Boolean(args['dry-run'])
|
|
|
|
if (!provider) {
|
|
throw new Error(
|
|
'Missing provider. Pass --provider or ADAPTER_TEST_RESULTS_PROVIDER.'
|
|
)
|
|
}
|
|
|
|
if (!secret) {
|
|
throw new Error(
|
|
'Missing secret. Pass --secret or ADAPTER_TEST_RESULTS_SECRET.'
|
|
)
|
|
}
|
|
|
|
if (!commitSha || !COMMIT_SHA_PATTERN.test(String(commitSha))) {
|
|
throw new Error(
|
|
'Invalid commit SHA. Pass --commit-sha (7-40 hex chars), ADAPTER_TEST_RESULTS_COMMIT_SHA, or GITHUB_SHA.'
|
|
)
|
|
}
|
|
|
|
const stat = await fs.stat(resultsRoot).catch(() => null)
|
|
if (!stat || !stat.isDirectory()) {
|
|
throw new Error(
|
|
`results-root does not exist or is not a directory: ${resultsRoot}`
|
|
)
|
|
}
|
|
|
|
console.log(`Collecting ${RESULT_FILE_SUFFIX} files from: ${resultsRoot}`)
|
|
const resultFiles = await collectResultFiles(resultsRoot)
|
|
|
|
if (resultFiles.length === 0) {
|
|
throw new Error(`No ${RESULT_FILE_SUFFIX} files found under ${resultsRoot}`)
|
|
}
|
|
|
|
console.log(`Found ${resultFiles.length} result files`)
|
|
const archive = await createArchive(resultsRoot, resultFiles)
|
|
console.log(`Created archive (${formatBytes(archive.byteLength)})`)
|
|
|
|
if (zipOutPath) {
|
|
await fs.mkdir(path.dirname(zipOutPath), { recursive: true })
|
|
await fs.writeFile(zipOutPath, archive)
|
|
console.log(`Wrote archive to: ${zipOutPath}`)
|
|
}
|
|
|
|
if (dryRun) {
|
|
console.log('Dry run complete (skipping upload).')
|
|
return
|
|
}
|
|
|
|
console.log(`Uploading to: ${endpoint}`)
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/zip',
|
|
'x-adapter-test-results-provider': String(provider),
|
|
'x-adapter-test-results-secret': String(secret),
|
|
'x-adapter-test-results-commit-sha': String(commitSha),
|
|
},
|
|
body: archive,
|
|
})
|
|
|
|
const responseText = await response.text()
|
|
let parsedResponse
|
|
|
|
try {
|
|
parsedResponse = JSON.parse(responseText)
|
|
} catch {
|
|
parsedResponse = responseText
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Upload failed (${response.status} ${response.statusText}): ${
|
|
typeof parsedResponse === 'string'
|
|
? parsedResponse
|
|
: JSON.stringify(parsedResponse)
|
|
}`
|
|
)
|
|
}
|
|
|
|
console.log('Upload succeeded')
|
|
if (typeof parsedResponse === 'string') {
|
|
console.log(parsedResponse)
|
|
} else {
|
|
console.log(JSON.stringify(parsedResponse, null, 2))
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error))
|
|
process.exit(1)
|
|
})
|