Files
Capsule/scripts/releaseArtifacts.mjs
2026-09-12 21:45:37 -04:00

65 lines
3.1 KiB
JavaScript

import { readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import { parse } from 'yaml'
import { validateFeedUrl } from '../build/config.mjs'
/** Hash an artifact for local tamper/corruption checks and upload verification. */
export function digest(data, algorithm = 'sha256', encoding = 'hex') {
return createHash(algorithm).update(data).digest(encoding)
}
/** Read only the x64 stable installer referenced by electron-builder metadata. */
export async function readReleaseArtifacts(directory) {
const metadata = await readFile(join(directory, 'latest.yml'))
const info = parse(metadata.toString('utf8'))
if (!/^\d+\.\d+\.\d+$/.test(info?.version) || info.files?.length !== 1) {
throw new Error('Expected one Windows x64 installer for a stable semantic version.')
}
const installer = `Capsule-${info.version}-x64-Setup.exe`
if (info.files[0].url !== installer || (info.path && info.path !== installer)) {
throw new Error('Release metadata must reference the expected local installer filename.')
}
const binary = await readFile(join(directory, installer))
if (info.files[0].sha512 !== digest(binary, 'sha512', 'base64') || info.files[0].size !== binary.length) {
throw new Error('Installer does not match latest.yml checksum or size.')
}
const blockmap = await readFile(join(directory, `${installer}.blockmap`))
return {
version: info.version,
files: [
{ name: installer, data: binary },
{ name: `${installer}.blockmap`, data: blockmap },
// Publish the pointer only after both immutable artifacts are available.
{ name: 'latest.yml', data: metadata }
]
}
}
/** Mark a successful signed build with hashes of its exact publishable files. */
export async function writeReleaseManifest(directory, feedUrl) {
// JSON is valid YAML. This gives the standalone PHP host a strict, dependency-free parser.
const metadataPath = join(directory, 'latest.yml')
const metadata = parse(await readFile(metadataPath, 'utf8'))
await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`)
const release = await readReleaseArtifacts(directory)
const manifest = {
version: release.version,
feedUrl: validateFeedUrl(feedUrl),
files: release.files.map(({ name, data }) => ({ name, sha256: digest(data) }))
}
await writeFile(join(directory, 'release-ready.json'), JSON.stringify(manifest, null, 2))
}
/** Refuse local builds, missing files, stale metadata, and changed release assets. */
export async function verifyRelease(directory) {
const manifest = JSON.parse(await readFile(join(directory, 'release-ready.json'), 'utf8'))
validateFeedUrl(manifest.feedUrl)
const release = await readReleaseArtifacts(directory)
if (release.version !== manifest.version || manifest.files?.length !== release.files.length ||
release.files.some(({ name, data }, index) => manifest.files[index].name !== name || manifest.files[index].sha256 !== digest(data))) {
throw new Error('Release artifacts have changed since the signed build. Rebuild before publishing.')
}
return { ...release, feedUrl: manifest.feedUrl }
}