Files
Capsule/scripts/uploadRelease.mjs
2026-09-12 21:21:28 -04:00

35 lines
1.7 KiB
JavaScript

import { digest } from './releaseArtifacts.mjs'
import { validateFeedUrl } from '../build/config.mjs'
/** Publish immutable assets first and the feed pointer last, verifying public bytes. */
export async function uploadRelease(release, { uploadUrl, token, fetchImpl = fetch, log = console.log }) {
uploadUrl = validateFeedUrl(uploadUrl)
if (!token || /[\r\n]/.test(token)) throw new Error('Set CAPSULE_UPLOAD_TOKEN in the release environment.')
for (const file of release.files) {
const metadata = file.name === 'latest.yml'
const response = await fetchImpl(new URL(file.name, uploadUrl), {
method: 'PUT',
redirect: 'error',
signal: AbortSignal.timeout(10 * 60 * 1000),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': metadata ? 'application/yaml' : 'application/octet-stream',
'Cache-Control': metadata ? 'no-cache' : 'public, max-age=31536000, immutable',
...(!metadata ? { 'If-None-Match': '*' } : {})
},
body: file.data
})
// Existing immutable files are safe to reuse only after verifying their public bytes.
if (!response.ok && !(response.status === 412 && !metadata)) {
throw new Error(`Upload failed for ${file.name}: HTTP ${response.status}. Publication stopped.`)
}
const publicResponse = await fetchImpl(new URL(file.name, release.feedUrl), {
redirect: 'error', signal: AbortSignal.timeout(10 * 60 * 1000), cache: 'no-store'
})
if (!publicResponse.ok || digest(Buffer.from(await publicResponse.arrayBuffer())) !== digest(file.data)) {
throw new Error(`Public verification failed for ${file.name}. Publication stopped.`)
}
log(`Published and verified ${file.name}`)
}
}