mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
feat(plugin-sdk): one-command publish (pack → release → preflight → PR)
Collapses the release into a single command: pack the artifact, tag + create the GitHub release, run the registry CI checks locally (preflight), and open the registry PR — stopping before it submits if preflight would fail, so a broken entry never becomes a doomed PR. `--sign` signs it; `--no-preflight` skips the gate. The individual pack/release/preflight/submit commands still exist. README + the Development/Publishing/Permissions wikis lead with `publish` now.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* trek-plugin publish — the whole release in one command: pack → tag + GitHub
|
||||
* release → preflight (the registry CI checks, locally) → open the registry PR.
|
||||
* If preflight fails it stops before submitting, so a broken entry never becomes
|
||||
* a doomed PR. This is the short path; the individual commands still exist for
|
||||
* when you want a step by hand.
|
||||
*
|
||||
* Requires `git` + `gh` (authenticated), same as `release`/`submit`.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { packPluginDir } from './pack.js';
|
||||
import { buildEntry } from './entry.js';
|
||||
import { preflight } from './preflight.js';
|
||||
import { submitEntry } from './submit.js';
|
||||
|
||||
function git(dir: string, args: string[], quiet = true): string {
|
||||
return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: quiet ? 'pipe' : 'inherit' }).toString().trim();
|
||||
}
|
||||
function tagExists(dir: string, tag: string): boolean {
|
||||
try { git(dir, ['rev-parse', `${tag}^{commit}`]); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
export async function publishPlugin(opts: {
|
||||
dir: string; repo: string; tag: string; now: string;
|
||||
signKeyPath?: string; registry?: string; draft?: boolean; notes?: string; skipPreflight?: boolean;
|
||||
}): Promise<{ prUrl: string }> {
|
||||
const dir = path.resolve(opts.dir);
|
||||
const step = (n: number, msg: string) => console.error(`[${n}/4] ${msg}`);
|
||||
|
||||
// 1. Pack
|
||||
step(1, 'Packing the artifact…');
|
||||
const zip = path.join(dir, 'plugin.zip');
|
||||
const packed = packPluginDir(dir, zip);
|
||||
console.error(` ✓ ${packed.files.length} files, ${packed.size} bytes`);
|
||||
|
||||
// 2. Tag (if needed) + push + GitHub release with the artifact attached
|
||||
step(2, `Tagging ${opts.tag} + creating the GitHub release…`);
|
||||
if (!tagExists(dir, opts.tag)) git(dir, ['tag', opts.tag]);
|
||||
try { git(dir, ['push', 'origin', opts.tag]); } catch {
|
||||
throw new Error(`could not push tag ${opts.tag} — is "origin" your plugin's GitHub repo and are you authenticated? (git push origin ${opts.tag})`);
|
||||
}
|
||||
try {
|
||||
execFileSync('gh', ['release', 'create', opts.tag, packed.artifact, '--repo', opts.repo, '--title', opts.tag, '--notes', opts.notes || `Release ${opts.tag}`], { stdio: 'pipe' });
|
||||
} catch {
|
||||
// Release already exists — (re)upload the packed artifact so its bytes match the pin.
|
||||
execFileSync('gh', ['release', 'upload', opts.tag, packed.artifact, '--repo', opts.repo, '--clobber'], { stdio: 'pipe' });
|
||||
}
|
||||
console.error(` ✓ release ${opts.tag} on ${opts.repo}`);
|
||||
|
||||
// 3. Build the entry, then run the registry CI checks locally
|
||||
const entry = buildEntry({ dir, repo: opts.repo, tag: opts.tag, zipPath: packed.artifact, signKeyPath: opts.signKeyPath, now: opts.now });
|
||||
if (opts.skipPreflight) {
|
||||
console.error('[3/4] Preflight skipped (--no-preflight).');
|
||||
} else {
|
||||
step(3, 'Preflight — running the registry CI checks…');
|
||||
const rep = await preflight(entry);
|
||||
for (const f of rep.failures) console.error(' ✗ ' + f);
|
||||
if (!rep.ok) throw new Error(`preflight found ${rep.failures.length} problem(s) — fix these and re-run (nothing was submitted). Did you push your code to ${opts.repo} before publishing?`);
|
||||
console.error(` ✓ all ${rep.passed.length} checks passed`);
|
||||
}
|
||||
|
||||
// 4. Open the registry PR
|
||||
step(4, 'Opening the registry PR…');
|
||||
const { prUrl } = submitEntry(entry, { registry: opts.registry, draft: opts.draft });
|
||||
console.error(' ✓ done');
|
||||
fs.rmSync(zip, { force: true });
|
||||
return { prUrl };
|
||||
}
|
||||
@@ -15,9 +15,11 @@
|
||||
* [--sign [key]] [--registry o/n] [--draft]
|
||||
* release [dir] --repo o/n --tag vX pack -> gh release -> print entry
|
||||
* [--sign [key]] [--merge entry.json]
|
||||
* publish [dir] --repo o/n --tag vX the lot: pack -> tag+release ->
|
||||
* [--sign [key]] [--no-preflight] preflight -> open the registry PR
|
||||
*
|
||||
* The goal: create -> dev -> release/submit, and never hand-compute sha256/size/
|
||||
* commitSha or hand-write the registry JSON.
|
||||
* The goal: create -> dev -> publish, and never hand-compute sha256/size/commitSha
|
||||
* or hand-write the registry JSON.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -29,6 +31,7 @@ import { scaffold, interactiveScaffold } from './create.js';
|
||||
import { runDev } from './dev.js';
|
||||
import { preflight } from './preflight.js';
|
||||
import { submitEntry } from './submit.js';
|
||||
import { publishPlugin } from './publish.js';
|
||||
import { generateKeypair, loadPrivateKey, signArtifact, publicKeyBase64, defaultKeyPath } from './sign.js';
|
||||
|
||||
const [cmd, ...args] = process.argv.slice(2);
|
||||
@@ -131,6 +134,15 @@ async function main(): Promise<void> {
|
||||
for (const f of rep.failures) console.error(' ✗ ' + f);
|
||||
if (!rep.ok) { console.error(`\n${rep.failures.length} check(s) would fail CI — fix these before submitting.`); process.exit(1); }
|
||||
console.error('\n✓ all checks passed — this entry should sail through CI.');
|
||||
} else if (cmd === 'publish') {
|
||||
if (!flags.repo || !flags.tag) fail('publish needs --repo <owner/name> and --tag <vX.Y.Z>');
|
||||
const { prUrl } = await publishPlugin({
|
||||
dir: pos[0] || '.', repo: flags.repo, tag: flags.tag,
|
||||
signKeyPath: signKey(), registry: flags.registry, draft: !!flags.draft,
|
||||
notes: flags.notes, skipPreflight: !!flags['no-preflight'], now: new Date().toISOString(),
|
||||
});
|
||||
console.error('\n✓ published — registry PR:');
|
||||
console.log(prUrl);
|
||||
} else if (cmd === 'submit') {
|
||||
if (!flags.repo || !flags.tag) fail('submit needs --repo <owner/name> and --tag <vX.Y.Z>');
|
||||
const entry = buildEntry({
|
||||
@@ -153,7 +165,7 @@ async function main(): Promise<void> {
|
||||
console.error('\nRegistry entry (add as registry/plugins/' + entry.id + '.json in a TREK-Plugins PR, or run `trek-plugin submit`):\n');
|
||||
process.stdout.write(JSON.stringify(entry, null, 2) + '\n');
|
||||
} else {
|
||||
console.error('usage: trek-plugin <create|dev|validate|pack|keygen|sign|entry|preflight|submit|release> [...]');
|
||||
console.error('usage: trek-plugin <create|dev|validate|pack|keygen|sign|entry|preflight|submit|release|publish> [...]');
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user