mirror of
https://github.com/smogon/sprites.git
synced 2026-10-26 18:15:16 -05:00
Draw a progress bar for deploy on a terminal
A deploy's last phase is a long silent wait: thousands of files packed into an upload or copied into a staging tree, with nothing between the line naming the command and the line after it finishes. Draw a bar over it when stderr is a terminal, and nothing at all when it isn't -- piped or under CI a redraw per file is noise in the log, and the stdout lines the deploy already prints are the record either way. The tar entries a pack hands out are counted by their entry callbacks, which wait on the pack draining into whoever is reading it, so the bar follows what the upload command has taken rather than what has been read off disk. The staging and -o paths count files as they land. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRBNA4JH27Eq3cgJpzCLV2
This commit is contained in:
@@ -163,7 +163,9 @@ export class ActionQueue {
|
||||
}
|
||||
}
|
||||
|
||||
async run(dir: string, mode: 'link' | 'copy' | 'tar', filter?: (dst: string) => boolean) {
|
||||
// `onFile` is called once per published name as it lands, for progress.
|
||||
async run(dir: string, mode: 'link' | 'copy' | 'tar', filter?: (dst: string) => boolean,
|
||||
onFile?: () => void) {
|
||||
if (!this.valid)
|
||||
throw new Error(`Invalid ActionQueue`);
|
||||
if (mode !== 'tar') {
|
||||
@@ -194,11 +196,12 @@ export class ActionQueue {
|
||||
await fs.rm(dst, {force: true});
|
||||
await fs.symlink(op.target, dst);
|
||||
}
|
||||
onFile?.();
|
||||
}
|
||||
} else {
|
||||
// In this case, I guess its a file rather than a dir.
|
||||
let out = createWriteStream(dir);
|
||||
(await this.pack(filter)).pipe(out);
|
||||
(await this.pack(filter, onFile)).pipe(out);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
out.on('error', reject);
|
||||
out.on('finish', () => resolve());
|
||||
@@ -206,23 +209,36 @@ export class ActionQueue {
|
||||
}
|
||||
}
|
||||
|
||||
async pack(filter?: (dst: string) => boolean): Promise<NodeJS.ReadableStream> {
|
||||
// Entries are handed to the pack as fast as they can be read, but each
|
||||
// one's callback waits on the pack draining into whoever is reading it,
|
||||
// so `onFile` tracks what the consumer has taken rather than what has
|
||||
// been queued for it.
|
||||
async pack(filter?: (dst: string) => boolean, onFile?: () => void)
|
||||
: Promise<NodeJS.ReadableStream> {
|
||||
if (!this.valid)
|
||||
throw new Error(`Invalid ActionQueue`);
|
||||
let t = tar.pack();
|
||||
// An entry's callback also carries the error the consumer is already
|
||||
// reporting; count the entry only where it made it through.
|
||||
let took = onFile === undefined ? undefined : (err?: Error | null) => {
|
||||
if (err === null || err === undefined) {
|
||||
onFile();
|
||||
}
|
||||
};
|
||||
for (let entry of this.log) {
|
||||
if (entry.type !== 'Op' || (filter !== undefined && !filter(entry.dst)))
|
||||
continue;
|
||||
let op = entry.op;
|
||||
if (op.type === 'Symlink') {
|
||||
t.entry({name: entry.dst, type: 'symlink', linkname: op.target}).on('error', () => {});
|
||||
t.entry({name: entry.dst, type: 'symlink', linkname: op.target}, took)
|
||||
.on('error', () => {});
|
||||
continue;
|
||||
}
|
||||
let data = op.type === 'Copy' ? await fs.readFile(op.src) : op.data;
|
||||
// A dying consumer destroys the pack and every pending entry
|
||||
// sink, and each sink emits the error; the consumer is the one
|
||||
// reporting the failure, so keep the sinks quiet.
|
||||
t.entry({name: entry.dst}, data).on('error', () => {});
|
||||
t.entry({name: entry.dst}, data, took).on('error', () => {});
|
||||
}
|
||||
// Without this the archive has no end-of-archive marker, and strict
|
||||
// readers (Python tarfile in stream mode) die on the truncation.
|
||||
|
||||
Reference in New Issue
Block a user