refactor: separate plugin repositories and release-based catalog
This commit is contained in:
+10
-86
@@ -1,87 +1,11 @@
|
||||
import { readFile, writeFile, mkdir, copyFile, rename, rm } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { parseArgs } from 'node:util';
|
||||
import path from 'node:path';
|
||||
import { root, output } from './suite.mjs';
|
||||
import { appendRelease, artifactPath } from './release-catalog.mjs';
|
||||
|
||||
const { values } = parseArgs({ options: {
|
||||
'base-url': { type: 'string' },
|
||||
catalog: { type: 'string', default: 'source' },
|
||||
entries: { type: 'string', default: 'dist/suite.json' },
|
||||
} });
|
||||
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 && !base.pathname.endsWith('/')) base.pathname += '/';
|
||||
const catalogDirectory = path.resolve(root, values.catalog);
|
||||
const publishDirectory = path.join(output, 'release');
|
||||
const timestamp = new Date().toISOString();
|
||||
const specifications = [
|
||||
{ file: 'index.json', id: 'trove-official', name: 'Trove 官方软件源' },
|
||||
];
|
||||
const catalogs = new Map();
|
||||
for (const spec of specifications) {
|
||||
let index;
|
||||
try { index = JSON.parse(await readFile(path.join(catalogDirectory, spec.file), 'utf8')); }
|
||||
catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
index = { schemaVersion: 1, id: spec.id, name: spec.name, generatedAt: timestamp, curios: [] };
|
||||
}
|
||||
if (index.id !== spec.id || index.schemaVersion !== 1 || !Array.isArray(index.curios)) throw new Error(`Unexpected catalog identity: ${spec.file}`);
|
||||
catalogs.set(spec.file, index);
|
||||
}
|
||||
const entries = JSON.parse(await readFile(path.resolve(root, values.entries), 'utf8'));
|
||||
const files = [];
|
||||
// Validate every proposed addition before changing any catalog or copying artifacts.
|
||||
for (const entry of entries) {
|
||||
if (entry.fault) continue;
|
||||
const bytes = await readFile(entry.file);
|
||||
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 = 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 });
|
||||
for (const [file, index] of catalogs) {
|
||||
const text = JSON.stringify(index, null, 2) + '\n';
|
||||
await writeFile(path.join(publishDirectory, file), text);
|
||||
const temporary = path.join(catalogDirectory, `${file}.tmp`);
|
||||
await writeFile(temporary, text);
|
||||
await rename(temporary, path.join(catalogDirectory, file));
|
||||
}
|
||||
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(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.');
|
||||
import { releasePlan } from './release-plan.mjs';
|
||||
const { values } = parseArgs({ options: { entries: { type: 'string' }, tag: { type: 'string' } } });
|
||||
if (!values.entries) throw new Error('--entries is required');
|
||||
const { files, descriptor } = await releasePlan(values.entries, values.tag);
|
||||
await rm('dist/release', { recursive: true, force: true });
|
||||
await mkdir('dist/release', { recursive: true });
|
||||
for (const [name, bytes] of files) await writeFile(`dist/release/${name}`, bytes);
|
||||
console.log(`Prepared ${files.size} release files in dist/release/`);
|
||||
console.log(`Create draft release ${descriptor.tag} at commit ${descriptor.commit}, upload every file, then publish.`);
|
||||
|
||||
Reference in New Issue
Block a user