61 lines
2.8 KiB
JavaScript
61 lines
2.8 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) {
|
|
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 }
|
|
}
|