From dd2396719db01593a4daf82cc52a5cb0ebeb98a6 Mon Sep 17 00:00:00 2001 From: Christopher Monsanto Date: Sun, 16 Aug 2026 18:21:26 -0400 Subject: [PATCH] Add pnpm deploy: pipe the assets tar to DEPLOY_COMMAND from .env The deploy tool grows a deploy subcommand that runs the build, packs the tar, and streams it to the command .env names. Packing now finalizes the archive; without the end-of-archive marker, strict stream readers (Python tarfile, hence smogonctl) die on truncation. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + package.json | 1 + tools/deploy/index.ts | 49 ++++++++++++++++++++++++++++++++++++++++++ tools/deploy/script.ts | 39 +++++++++++++++++++++------------ 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 65571d87..e12159ad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ pnpm-debug.log .cache/ .build/ build.config +.env diff --git a/package.json b/package.json index 2ddfc7b0..396f215b 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "type": "module", "scripts": { "build": "node tools/build/index.ts", + "deploy": "node tools/deploy/index.ts deploy assets.deploy.js", "check": "tsc --build tsconfig-workspace.json" }, "dependencies": { diff --git a/tools/deploy/index.ts b/tools/deploy/index.ts index 3082fa88..a25374e6 100644 --- a/tools/deploy/index.ts +++ b/tools/deploy/index.ts @@ -2,6 +2,7 @@ import program from 'commander'; import * as script from './script.ts'; import nodePath from 'path'; +import {spawn, type ChildProcess} from 'child_process'; function collect(value : string, previous : string[]) { return previous.concat([value]); @@ -72,6 +73,54 @@ program await runAq(aq, tar ? 'tar' : link ? 'link' : 'copy', outputDir, verbose); }); +function waitExit(child : ChildProcess) : Promise { + return new Promise((resolve, reject) => { + child.on('error', reject); + child.on('close', code => resolve(code)); + }); +} + +program + .command('deploy [scripts...]') + .option('-v, --verbose', 'Verbose') + .action(async (scripts : string[], {verbose}) => { + try { + process.loadEnvFile('.env'); + } catch { + console.error(`missing .env; set DEPLOY_COMMAND="ssh smogon smogonctl assets upload"`); + process.exit(1); + } + const command = process.env.DEPLOY_COMMAND; + if (command === undefined) { + console.error(`DEPLOY_COMMAND not set in .env`); + process.exit(1); + } + + const build = spawn('pnpm', ['build'], {stdio: ['ignore', 'inherit', 'inherit']}); + if (await waitExit(build) !== 0) { + process.exit(1); + } + + const aq = new script.ActionQueue; + for (const file of scripts) { + const scr = new script.Script(file, 'file'); + script.run(scr, nodePath.dirname(file), aq); + } + if (!aq.valid) { + aq.print(verbose ? 'all' : 'errors'); + process.exit(1); + } + + const upload = spawn(command, {shell: true, stdio: ['pipe', 'inherit', 'inherit']}); + // If the command dies early we report its exit code; don't also crash + // on the resulting EPIPE. + upload.stdin!.on('error', () => {}); + aq.pack().pipe(upload.stdin!); + if (await waitExit(upload) !== 0) { + process.exit(1); + } + }); + program.parse(process.argv); if (process.argv.slice(2).length === 0) { diff --git a/tools/deploy/script.ts b/tools/deploy/script.ts index 259d80b9..3358afb3 100644 --- a/tools/deploy/script.ts +++ b/tools/deploy/script.ts @@ -151,24 +151,35 @@ export class ActionQueue { } } } else { - let t = tar.pack(); - for (const entry of this.log) { - if (entry.type !== 'Op') - continue; - const op = entry.op; - if (op.type === 'Copy'){ - t.entry({name: entry.dst}, fs.readFileSync(op.src)); - } else if (op.type === 'Write') { - t.entry({name: entry.dst}, op.data); - } - } // In this case, I guess its a file rather than a dir. - t.pipe(fs.createWriteStream(dir)) - return new Promise(resolve => { - t.on('close', () => resolve()) + const out = fs.createWriteStream(dir); + this.pack().pipe(out); + return new Promise((resolve, reject) => { + out.on('error', reject); + out.on('finish', () => resolve()); }) } } + + pack() : NodeJS.ReadableStream { + if (!this.valid) + throw new Error(`Invalid ActionQueue`); + let t = tar.pack(); + for (const entry of this.log) { + if (entry.type !== 'Op') + continue; + const op = entry.op; + if (op.type === 'Copy'){ + t.entry({name: entry.dst}, fs.readFileSync(op.src)); + } else if (op.type === 'Write') { + t.entry({name: entry.dst}, op.data); + } + } + // Without this the archive has no end-of-archive marker, and strict + // readers (Python tarfile in stream mode) die on the truncation. + t.finalize(); + return t; + } } export class Script extends vm.Script {