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 <noreply@anthropic.com>
This commit is contained in:
Christopher Monsanto
2026-08-16 18:21:26 -04:00
parent 41119f63a3
commit dd2396719d
4 changed files with 76 additions and 14 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ pnpm-debug.log
.cache/
.build/
build.config
.env

View File

@@ -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": {

View File

@@ -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<number | null> {
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) {

View File

@@ -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<void>(resolve => {
t.on('close', () => resolve())
const out = fs.createWriteStream(dir);
this.pack().pipe(out);
return new Promise<void>((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 {