fix: add job cancel and tune artifact download

This commit is contained in:
Codex
2026-06-02 09:20:43 +00:00
parent 263b0cf3b2
commit 66ec934160
5 changed files with 61 additions and 16 deletions
@@ -16,7 +16,7 @@ assertCondition(source.includes("downloadRemoteFile(options, remoteArchive, loca
assertCondition(source.includes("runRemoteScriptBackground(options, remoteScript"), "remote docker save must run as a background job");
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"'), "artifact ssh download must use the stable default bounded chunk size, not the largest chunk");
assertCondition(downloadRemoteFileSource.includes('"--chunk-bytes",\n "64000"'), "artifact ssh download must use a mid-size bounded chunk, not the largest chunk");
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({
@@ -26,7 +26,7 @@ console.log(JSON.stringify({
"no docker-save stdout stream over ssh",
"compose artifact uses verified ssh download",
"remote docker save and k3s deploy use background polling",
"artifact downloads use the stable default bounded ssh chunk size",
"artifact downloads use a mid-size bounded ssh chunk",
"dev frontend artifact deploy syncs scoped ssh runtime keys"
]
}, null, 2));
+6 -1
View File
@@ -3,7 +3,7 @@ import { debugDispatch, debugHealth, debugTask, isDebugDispatchCommand, type Deb
import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStack, stopStack, unsupportedRebuildService } from "./src/docker";
import { parseE2ERunOptions, runE2E } from "./src/e2e";
import { emitError, emitJson } from "./src/output";
import { jobWithTail, listJobs, listJobsSummary, readJob, runJob } from "./src/jobs";
import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, runJob } from "./src/jobs";
import { checkHelp, parseCheckOptions, runChecks, runRecoveryGuardrailsCheck } from "./src/check";
import { runSsh } from "./src/ssh";
import { autoRemoteCiPublishUserServiceDryRunPlan, extractRemoteCliOptions, runRemoteCli } from "./src/remote";
@@ -469,6 +469,11 @@ async function main(): Promise<void> {
emitJson(commandName, { job: jobWithTail(readJob(id), boundedNumberOption("--tail-bytes", 12000, 500_000)) });
return;
}
if (sub === "cancel") {
if (!third) throw new Error("job cancel requires job id");
emitJson(commandName, cancelJob(third));
return;
}
}
if (top === "debug") {
+2
View File
@@ -1677,6 +1677,8 @@ function downloadRemoteFile(options: ArtifactRegistryOptions, remotePath: string
"ssh",
options.providerId,
"download",
"--chunk-bytes",
"64000",
remotePath,
localPath,
], repoRoot, { timeoutMs });
+4 -2
View File
@@ -84,6 +84,7 @@ export function rootHelp(): unknown {
{ command: "codex (queues [--full|--all] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List low-noise queue summaries by default, including effective activity counts that distinguish scheduler-local queues, DB running tasks, and heartbeat-fresh runners; full queue rows require --full/--all." },
{ command: "job list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page and progress summaries." },
{ command: "job status <jobId|latest> [--tail-bytes N]", description: "Show job state with a structured progress summary and bounded stdout/stderr tails." },
{ command: "job cancel <jobId>", description: "Cancel a queued/running async job through the .state/jobs control entry and keep a terminal canceled record." },
{ command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." },
{ command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." },
{ command: "debug task <taskId|latest>", description: "Read a dispatched task record from internal core for CLI debugging." },
@@ -480,13 +481,14 @@ function codexHelp(): unknown {
function jobHelp(): unknown {
return {
command: "job list|status",
command: "job list|status|cancel",
output: "json",
usage: [
"bun scripts/cli.ts job list [--limit N] [--include-command]",
"bun scripts/cli.ts job status <jobId|latest> [--tail-bytes N]",
"bun scripts/cli.ts job cancel <jobId>",
],
description: "Inspect fire-and-forget job state from .state/jobs with structured progress summaries and bounded log tails.",
description: "Inspect or cancel fire-and-forget job state from .state/jobs with structured progress summaries and bounded log tails.",
};
}
+47 -11
View File
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { repoRoot, rootPath } from "./config";
import { runCommandToFiles, tailFile } from "./command";
export type JobStatus = "queued" | "running" | "succeeded" | "failed";
export type JobStatus = "queued" | "running" | "succeeded" | "failed" | "canceled";
export interface JobRecord {
id: string;
@@ -68,6 +68,34 @@ export function readJob(id: string): JobRecord {
return JSON.parse(readFileSync(path, "utf8")) as JobRecord;
}
export function cancelJob(id: string): unknown {
const job = readJob(id);
if (job.status !== "queued" && job.status !== "running") {
return { ok: true, action: "cancel-job", id, alreadyTerminal: true, job };
}
const actions: Array<Record<string, unknown>> = [];
if (job.runnerPid !== null && job.runnerPid !== undefined) {
try {
process.kill(-job.runnerPid, "SIGTERM");
actions.push({ signal: "SIGTERM", target: "process-group", pid: job.runnerPid, ok: true });
} catch (error) {
actions.push({ signal: "SIGTERM", target: "process-group", pid: job.runnerPid, ok: false, error: error instanceof Error ? error.message : String(error) });
try {
process.kill(job.runnerPid, "SIGTERM");
actions.push({ signal: "SIGTERM", target: "process", pid: job.runnerPid, ok: true });
} catch (innerError) {
actions.push({ signal: "SIGTERM", target: "process", pid: job.runnerPid, ok: false, error: innerError instanceof Error ? innerError.message : String(innerError) });
}
}
}
job.status = "canceled";
job.finishedAt = new Date().toISOString();
job.exitCode = 130;
writeFileSync(job.stderrFile, `${tailFile(job.stderrFile, 512_000)}${JSON.stringify({ event: "unidesk.job.cancel", at: job.finishedAt, jobId: id, actions })}\n`, "utf8");
writeJob(job);
return { ok: true, action: "cancel-job", id, actions, job };
}
export function listJobs(): JobRecord[] {
return readdirSync(jobsDir())
.filter((name) => name.endsWith(".json"))
@@ -323,32 +351,40 @@ function summarizeGitMirrorJobProgress(job: JobRecord, stdoutTail: string, stder
function genericJobProgress(job: JobRecord): JobProgressSummary {
const nowMs = Date.now();
const stderrTail = tailFile(job.stderrFile, 96_000);
const downloadEvents = parseJsonLineEvents(stderrTail, "unidesk.ssh.download.progress");
const lastDownload = downloadEvents.at(-1) ?? null;
const lastDownloadAt = stringField(lastDownload?.at);
const lastEventAgeSeconds = lastDownloadAt === null ? null : secondsSince(lastDownloadAt, job.finishedAt ?? nowMs);
const elapsedSeconds = jobElapsedSeconds(job, nowMs);
const warnings = jobProgressWarnings({
job,
eventsObserved: 0,
eventsObserved: downloadEvents.length,
elapsedSeconds,
stage: null,
stageStatus: null,
stage: lastDownload === null ? null : "ssh-download",
stageStatus: lastDownload === null ? null : "running",
stageElapsedSeconds: null,
lastEventAgeSeconds: null,
lastEventAgeSeconds,
});
const downloadSummary = lastDownload === null
? null
: ` ssh-download ${Number(lastDownload.actualBytes ?? 0)}/${Number(lastDownload.expectedBytes ?? 0)} bytes chunks=${Number(lastDownload.chunks ?? 0)}`;
return {
kind: "generic",
stage: null,
stageStatus: null,
stage: lastDownload === null ? null : "ssh-download",
stageStatus: lastDownload === null ? null : "running",
sourceCommit: null,
pipelineRun: null,
pipelineCreated: null,
elapsedSeconds,
stageElapsedSeconds: null,
lastEventAt: null,
lastEventAgeSeconds: null,
eventsObserved: 0,
lastEventAt: lastDownloadAt,
lastEventAgeSeconds,
eventsObserved: downloadEvents.length,
slow: warnings.length > 0,
warnings,
timings: {},
summary: `${job.status}${job.exitCode === null ? "" : ` exit=${job.exitCode}`}`,
summary: `${job.status}${job.exitCode === null ? "" : ` exit=${job.exitCode}`}${downloadSummary ?? ""}`,
nextCommand: job.status === "running" ? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000` : null,
};
}