diff --git a/scripts/artifact-registry-ssh-timeout-contract-test.ts b/scripts/artifact-registry-ssh-timeout-contract-test.ts index b88d4e23..48bc1d77 100644 --- a/scripts/artifact-registry-ssh-timeout-contract-test.ts +++ b/scripts/artifact-registry-ssh-timeout-contract-test.ts @@ -17,6 +17,7 @@ assertCondition(source.includes("runRemoteScriptBackground(options, remoteScript assertCondition(source.includes('runRemoteScriptBackground(options, deployScript, Math.max(options.timeoutMs, 420_000), "d601-k3s-deploy")'), "D601 k3s deploy must use background polling"); assertCondition(source.includes('"ssh",\n options.providerId,\n "download"'), "download helper must route through UniDesk ssh download"); assertCondition(downloadRemoteFileSource.includes('"--chunk-bytes",\n "64000"'), "artifact ssh download must use a mid-size bounded chunk, not the largest chunk"); +assertCondition(downloadRemoteFileSource.includes("teeStderrFile: process.env.UNIDESK_JOB_STDERR_FILE"), "artifact ssh download must stream progress stderr into async job logs"); assertCondition(source.includes("UNIDESK_SSH_CLIENT_TOKEN") && source.includes("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST"), "dev frontend artifact deploy must sync scoped ssh runtime keys"); console.log(JSON.stringify({ @@ -27,6 +28,7 @@ console.log(JSON.stringify({ "compose artifact uses verified ssh download", "remote docker save and k3s deploy use background polling", "artifact downloads use a mid-size bounded ssh chunk", + "artifact download progress is visible in async job stderr", "dev frontend artifact deploy syncs scoped ssh runtime keys" ] }, null, 2)); diff --git a/scripts/src/artifact-registry.ts b/scripts/src/artifact-registry.ts index c45b76a8..3fb3fe34 100644 --- a/scripts/src/artifact-registry.ts +++ b/scripts/src/artifact-registry.ts @@ -1671,7 +1671,7 @@ function combineCommandResults(command: string[], parts: CommandResult[]): Comma } function downloadRemoteFile(options: ArtifactRegistryOptions, remotePath: string, localPath: string, timeoutMs = options.timeoutMs): CommandResult { - return runCommand([ + const result = runCommand([ process.execPath, "scripts/cli.ts", "ssh", @@ -1681,7 +1681,12 @@ function downloadRemoteFile(options: ArtifactRegistryOptions, remotePath: string "64000", remotePath, localPath, - ], repoRoot, { timeoutMs }); + ], repoRoot, { + timeoutMs, + teeStdoutFile: process.env.UNIDESK_JOB_STDOUT_FILE, + teeStderrFile: process.env.UNIDESK_JOB_STDERR_FILE, + }); + return result; } async function runRemoteScriptBackground( diff --git a/scripts/src/command.ts b/scripts/src/command.ts index 0828a3d1..331c67a5 100644 --- a/scripts/src/command.ts +++ b/scripts/src/command.ts @@ -1,5 +1,5 @@ import { spawn, spawnSync } from "node:child_process"; -import { closeSync, createWriteStream, existsSync, openSync, readSync, statSync } from "node:fs"; +import { appendFileSync, closeSync, createWriteStream, existsSync, openSync, readSync, statSync } from "node:fs"; export interface CommandResult { command: string[]; @@ -11,7 +11,7 @@ export interface CommandResult { timedOut: boolean; } -export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): CommandResult { +export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv; teeStdoutFile?: string; teeStderrFile?: string } = {}): CommandResult { const result = spawnSync(command[0], command.slice(1), { cwd, encoding: "utf8", @@ -20,12 +20,15 @@ export function runCommand(command: string[], cwd: string, options: { timeoutMs? timeout: options.timeoutMs, }); const error = result.error as (Error & { code?: string }) | undefined; + if (options.teeStdoutFile !== undefined && result.stdout !== undefined && result.stdout.length > 0) appendFileSync(options.teeStdoutFile, result.stdout, "utf8"); + const stderr = result.stderr ?? error?.message ?? ""; + if (options.teeStderrFile !== undefined && stderr.length > 0) appendFileSync(options.teeStderrFile, stderr, "utf8"); return { command, cwd, exitCode: result.status, stdout: result.stdout ?? "", - stderr: result.stderr ?? error?.message ?? "", + stderr, signal: result.signal, timedOut: error?.code === "ETIMEDOUT", }; @@ -39,7 +42,7 @@ export async function runCommandToFiles(command: string[], cwd: string, stdoutFi const stdout = createWriteStream(stdoutFile, { flags: "a" }); const stderr = createWriteStream(stderrFile, { flags: "a" }); stdout.write(`$ ${command.map((part) => JSON.stringify(part)).join(" ")}\n`); - const child = spawn(command[0], command.slice(1), { cwd, env: process.env }); + const child = spawn(command[0], command.slice(1), { cwd, env: { ...process.env, UNIDESK_JOB_STDOUT_FILE: stdoutFile, UNIDESK_JOB_STDERR_FILE: stderrFile } }); child.stdout.pipe(stdout, { end: false }); child.stderr.pipe(stderr, { end: false }); const exitCode = await new Promise((resolve) => {