Files
curios/tools/release-plan.mjs
T

46 lines
2.8 KiB
JavaScript

import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { valid } from 'semver';
export async function releasePlan(entriesFile, tag) {
const config = JSON.parse(await readFile('release.json', 'utf8'));
const repository = config.repository;
if (!tag || !/^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/.test(tag)) throw new Error('A release tag is required');
const commit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
const entries = JSON.parse(await readFile(entriesFile, 'utf8')).filter(entry => !entry.fault);
if (!entries.length) throw new Error('No packages to publish');
const plugins = new Map(); const files = new Map();
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
for (const entry of entries) {
const manifest = entry.manifest;
if (!config.ids.includes(manifest.id) || !valid(manifest.version)) throw new Error('Unexpected plugin identity/version');
if (!config.suite && tag !== `v${manifest.version}`) throw new Error('Release tag must match curio.json');
if (!['any', 'aarch64-apple-darwin', 'x86_64-apple-darwin'].includes(entry.artifact.target)) throw new Error('Unsupported package target');
const bytes = await readFile(entry.file);
if (digest(bytes) !== entry.artifact.sha256 || bytes.length !== entry.artifact.size) throw new Error('Package differs from build metadata');
const asset = `${manifest.id}-${manifest.version}-${entry.artifact.target}.curio`;
if (files.has(asset)) throw new Error(`Duplicate release asset: ${asset}`);
files.set(asset, bytes);
const key = `${manifest.id}@${manifest.version}`;
if (!plugins.has(key)) {
let icon;
if (manifest.icon) {
const file = path.resolve(entry.directory, manifest.icon);
if (!file.startsWith(path.resolve(entry.directory) + path.sep)) throw new Error('Icon escapes package directory');
const data = await readFile(file);
const type = { '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp' }[path.extname(file)];
if (type && data.length <= 64 * 1024) icon = `data:${type};base64,${data.toString('base64')}`;
}
plugins.set(key, { id: manifest.id, name: manifest.name, description: manifest.description ?? '', icon,
version: manifest.version, engine: manifest.engine, artifacts: [] });
}
plugins.get(key).artifacts.push({ target: entry.artifact.target, asset, sha256: entry.artifact.sha256, size: bytes.length });
}
const descriptor = Buffer.from(JSON.stringify({ schemaVersion: 1, repository, tag, commit, plugins: [...plugins.values()] }, null, 2) + '\n');
files.set('trove-release.json', descriptor);
return { files, descriptor: JSON.parse(descriptor) };
}