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}`) } }