feat: publish Curios directly through the Git source

This commit is contained in:
2026-09-09 20:17:55 +08:00
parent 68b7164df8
commit 72272eeb1a
17 changed files with 262 additions and 35 deletions
+24 -7
View File
@@ -9,12 +9,11 @@ const { values } = parseArgs({ options: {
'base-url': { type: 'string' },
catalog: { type: 'string', default: 'source' },
} });
if (!values['base-url']) throw new Error('Usage: pnpm run release:prepare --base-url https://downloads.example/source/');
const base = new URL(values['base-url']);
if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash) {
const base = values['base-url'] ? new URL(values['base-url']) : null;
if (base && (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash)) {
throw new Error('Publication base must be an HTTPS directory URL without credentials, query or fragment');
}
if (!base.pathname.endsWith('/')) base.pathname += '/';
if (base && !base.pathname.endsWith('/')) base.pathname += '/';
const catalogDirectory = path.resolve(root, values.catalog);
const publishDirectory = path.join(output, 'release');
const timestamp = new Date().toISOString();
@@ -41,18 +40,36 @@ for (const entry of entries) {
const sha256 = createHash('sha256').update(bytes).digest('hex');
if (sha256 !== entry.artifact.sha256 || bytes.length !== entry.artifact.size) throw new Error(`Build metadata doesn't match ${entry.file}; run pack again`);
const relative = artifactPath(entry.manifest, entry.artifact.target);
const url = new URL(relative.split('/').map(encodeURIComponent).join('/'), base).href;
const url = base ? new URL(relative.split('/').map(encodeURIComponent).join('/'), base).href : relative;
const spec = specifications[0];
const artifact = { ...entry.artifact, url };
catalogs.set(spec.file, appendRelease(catalogs.get(spec.file), entry.manifest, artifact, timestamp));
files.push({ from: entry.file, relative, sha256, size: bytes.length, url });
}
// Git publication keeps immutable packages alongside the canonical index.
if (!base) {
for (const file of files) {
try {
const existing = await readFile(path.join(catalogDirectory, file.relative));
if (existing.length !== file.size || createHash('sha256').update(existing).digest('hex') !== file.sha256) {
throw new Error(`Published artifact differs: ${file.relative}; bump the plugin version`);
}
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
}
// Only the current approved publication plan belongs in the generated upload directory.
await rm(publishDirectory, { recursive: true, force: true });
for (const file of files) {
const destination = path.join(publishDirectory, file.relative);
await mkdir(path.dirname(destination), { recursive: true });
await copyFile(file.from, destination);
if (!base) {
const published = path.join(catalogDirectory, file.relative);
await mkdir(path.dirname(published), { recursive: true });
await copyFile(file.from, published);
}
}
await mkdir(catalogDirectory, { recursive: true });
await mkdir(publishDirectory, { recursive: true });
@@ -63,7 +80,7 @@ for (const [file, index] of catalogs) {
await writeFile(temporary, text);
await rename(temporary, path.join(catalogDirectory, file));
}
await writeFile(path.join(publishDirectory, 'upload-plan.json'), JSON.stringify({ baseUrl: base.href, files }, null, 2) + '\n');
await writeFile(path.join(publishDirectory, 'upload-plan.json'), JSON.stringify({ kind: base ? 'http' : 'git', baseUrl: base?.href, files }, null, 2) + '\n');
console.log(`Prepared ${files.length} immutable artifacts in ${publishDirectory}`);
console.log(`Catalog history: ${catalogDirectory}`);
console.log('Next: upload package files without overwrite, verify remote bytes, then atomically publish index.json.');
console.log(base ? 'Next: upload package files without overwrite, verify remote bytes, then atomically publish index.json.' : 'Next: commit source/index.json and source/packages/ together, push the repository, then verify the Git source.');