Files
open-science/scripts/verify-runtime-bundle.mjs
Ewen 5a818d73b9 ci(pipelines): optimize nightly and release workflows (#944)
* ci(pipelines): optimize nightly and release workflows

Batch Nightly builds hourly, isolate non-cancellable publishing and advisory Windows upgrade validation, and shorten Windows/full release paths without weakening publish gates.

* ci(nightly): avoid privileged source checkout

Prepare certification metadata in the read-only Nightly run so the workflow_run publisher only downloads and publishes artifacts.

* ci(nightly): prevent stale publication rollback

Compare the candidate build with the currently published nightly commit after acquiring the publication lock, skipping older candidates and failing closed on divergent history.
2026-08-07 16:04:00 -07:00

111 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { pathToFileURL } from 'node:url'
import { setTimeout as sleep } from 'node:timers/promises'
import { VERSIONS, packArchiveFile, packId, readDefaultEnvVersion } from './stage-default-envs.mjs'
export const DEFAULT_CDN_BASE = 'https://statics.aipoch.com/open-science'
export const DEFAULT_MAX_ATTEMPTS = 3
export const DEFAULT_RETRY_DELAY_MS = 1000
const TRANSIENT_HTTP_STATUSES = new Set([403, 408, 425, 429, 500, 502, 503, 504])
export const runtimeManifestUrl = (cdnBase, envVersion, subdir) =>
`${cdnBase.replace(/\/+$/, '')}/runtime-bundle/${envVersion}/${subdir}/manifest.json`
export const validatePublishedManifest = (manifest, envVersion, subdir) => {
if (!manifest || manifest.schema !== 1) throw new Error('manifest schema must be 1')
if (manifest.envVersion !== envVersion) {
throw new Error(`manifest envVersion ${manifest.envVersion} does not match ${envVersion}`)
}
if (manifest.subdir !== subdir) {
throw new Error(`manifest subdir ${manifest.subdir} does not match ${subdir}`)
}
for (const [language, versions] of Object.entries(VERSIONS)) {
for (const version of versions) {
const id = packId(language, version)
const entry = manifest.packs?.[id]
if (!entry) throw new Error(`manifest is missing ${id}`)
if (
entry.language !== language ||
entry.version !== version ||
entry.file !== packArchiveFile(language, version)
) {
throw new Error(`manifest has an invalid canonical entry for ${id}`)
}
if (
!/^[a-f0-9]{64}$/.test(entry.sha256) ||
!Number.isInteger(entry.size) ||
entry.size <= 0
) {
throw new Error(`manifest has invalid integrity metadata for ${id}`)
}
}
}
}
export const verifyRuntimeBundle = async (
cdnBase,
envVersion,
subdirs,
{
fetchImpl = fetch,
sleepImpl = sleep,
maxAttempts = DEFAULT_MAX_ATTEMPTS,
retryDelayMs = DEFAULT_RETRY_DELAY_MS
} = {}
) => {
for (const subdir of subdirs) {
const url = runtimeManifestUrl(cdnBase, envVersion, subdir)
let response
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
response = await fetchImpl(url, { cache: 'no-store' })
} catch (error) {
if (attempt === maxAttempts) {
throw new Error(
`${url} request failed after ${attempt} attempts: ${error instanceof Error ? error.message : String(error)}`
)
}
console.warn(`[runtime-bundle] ${url} request failed; retrying (${attempt}/${maxAttempts})`)
await sleepImpl(retryDelayMs * attempt)
continue
}
if (response.ok) break
if (!TRANSIENT_HTTP_STATUSES.has(response.status) || attempt === maxAttempts) {
throw new Error(`${url} returned HTTP ${response.status} after ${attempt} attempt(s)`)
}
console.warn(
`[runtime-bundle] ${url} returned HTTP ${response.status}; retrying (${attempt}/${maxAttempts})`
)
await sleepImpl(retryDelayMs * attempt)
}
const manifest = await response.json()
validatePublishedManifest(manifest, envVersion, subdir)
console.log(`[runtime-bundle] verified ${url}`)
}
}
const main = async () => {
const subdirs = process.argv.slice(2)
if (subdirs.length === 0) {
throw new Error('usage: node scripts/verify-runtime-bundle.mjs <subdir> [subdir...]')
}
await verifyRuntimeBundle(
process.env.OPEN_SCIENCE_ENV_CDN_BASE || DEFAULT_CDN_BASE,
readDefaultEnvVersion(),
subdirs
)
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
console.error(`[runtime-bundle] ${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
})
}