Files
hermes-agent/tests/install/e2e-assets/bundle-inputs.mjs

67 lines
3.4 KiB
JavaScript

#!/usr/bin/env node
// Pin real release packages before a destructive update leg starts.
import { createHash } from 'node:crypto'
import { createWriteStream } from 'node:fs'
import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { Transform, Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { parseArgs } from 'node:util'
import { pathToFileURL } from 'node:url'
import { FORMATS, SHA256, artifactUrl, validateBundleInputs } from './bundle-manifest.cjs'
export async function downloadArtifact(artifact, destination) {
const url = artifactUrl(artifact.url)
const response = await fetch(url, { signal: AbortSignal.timeout(30 * 60 * 1000) })
if (!response.ok || !response.body) throw new Error(`Package download returned HTTP ${response.status}`)
const digest = createHash('sha256')
const temporary = `${destination}.partial`
try {
await pipeline(Readable.fromWeb(response.body), new Transform({
transform(chunk, encoding, done) { digest.update(chunk); done(null, chunk) }
}), createWriteStream(temporary, { flags: 'wx' }))
if (digest.digest('hex') !== artifact.sha256) throw new Error('Package SHA-256 mismatch')
await rename(temporary, destination)
} catch (error) {
await rm(temporary, { force: true })
throw error
}
}
export async function stageBundleInputs({ manifestUrl, platform, arch, out, expectedCommit, manifestSha256 }) {
const response = await fetch(artifactUrl(manifestUrl), { signal: AbortSignal.timeout(60_000) })
if (!response.ok) throw new Error(`Manifest download returned HTTP ${response.status}`)
const text = await response.text()
if (text.length > 1024 * 1024) throw new Error('Bundle input manifest is too large')
if (manifestSha256 && (!SHA256.test(manifestSha256) || createHash('sha256').update(text).digest('hex') !== manifestSha256)) {
throw new Error('Transition manifest SHA-256 mismatch')
}
const manifest = validateBundleInputs(JSON.parse(text), platform, arch)
if (expectedCommit && manifest.new.commit !== expectedCommit) {
throw new Error('Candidate commit must equal the tested workflow SHA')
}
await mkdir(out, { recursive: true })
const staged = { ...manifest }
for (const slot of ['old', 'new']) {
const destination = path.resolve(out, `${slot}${FORMATS[platform]}`)
await downloadArtifact(manifest[slot].artifact, destination)
staged[slot] = { ...manifest[slot], artifact: { ...manifest[slot].artifact, path: destination } }
}
const filename = path.resolve(out, 'bundle-inputs.json')
await writeFile(filename, JSON.stringify(staged, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' })
return filename
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
const { values } = parseArgs({ options: {
'manifest-url': { type: 'string' }, platform: { type: 'string' },
arch: { type: 'string' }, out: { type: 'string' }
} })
if (!values['manifest-url'] || !values.platform || !values.arch || !values.out) {
throw new Error('--manifest-url, --platform, --arch and --out are required')
}
console.log(await stageBundleInputs({ manifestUrl: values['manifest-url'], platform: values.platform, arch: values.arch, out: values.out,
expectedCommit: process.env.GITHUB_ACTIONS === 'true' ? process.env.GITHUB_SHA : undefined,
manifestSha256: process.env.BUNDLE_MANIFEST_SHA256 || undefined }))
}