fix: use event-driven renderer for build progress
This commit is contained in:
+9
-3
@@ -738,7 +738,7 @@ export async function runUp(
|
||||
};
|
||||
const workspacePath = `/workspaces/${encodeURIComponent(project)}`;
|
||||
|
||||
const taskCtx = await new Listr<UpContext>(
|
||||
const taskCtx = await new Listr<UpContext, "default" | "simple">(
|
||||
[
|
||||
{
|
||||
title: "Read compose",
|
||||
@@ -796,7 +796,7 @@ export async function runUp(
|
||||
cwd,
|
||||
{
|
||||
progress: (message) => {
|
||||
task.output = message;
|
||||
if (task.output !== message) task.output = message;
|
||||
},
|
||||
service: (name) => {
|
||||
let stream: ReturnType<ListrTaskWrapper<UpContext, any, any>["stdout"]> | undefined;
|
||||
@@ -1071,7 +1071,13 @@ export async function runUp(
|
||||
},
|
||||
},
|
||||
],
|
||||
{ rendererOptions: { collapseErrors: false } },
|
||||
{
|
||||
// Build progress can remain queued across many polls. The default TTY
|
||||
// spinner redraws every child on every tick, which duplicates those
|
||||
// frames in captured terminal output. Simple emits only task events.
|
||||
renderer: build ? "simple" : "default",
|
||||
rendererOptions: { collapseErrors: false },
|
||||
},
|
||||
).run();
|
||||
|
||||
return taskCtx;
|
||||
|
||||
@@ -26,6 +26,56 @@ const snapshot = {
|
||||
};
|
||||
|
||||
describe("up API pipeline", () => {
|
||||
test("does not append queued build frames on repeated TTY progress ticks", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-up-api-"));
|
||||
const previousCwd = process.cwd();
|
||||
const tty = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
|
||||
const rendered: string[] = [];
|
||||
const writes = spyOn(process.stdout, "write").mockImplementation(((chunk: string | Uint8Array) => {
|
||||
rendered.push(String(chunk));
|
||||
return true;
|
||||
}) as typeof process.stdout.write);
|
||||
let polls = 0;
|
||||
try {
|
||||
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true });
|
||||
await writeFile(join(root, "compose.yml"), "services:\n app:\n build: .\n");
|
||||
await writeFile(join(root, ".kuberrc.ts"), 'export default { project: "shop" };\n');
|
||||
process.chdir(root);
|
||||
const trust = await resolveTrustIdentity("shop", root);
|
||||
const request: ApiRequester = async <T>(path: string) => {
|
||||
if (path === "/snapshots/negotiate") return { ready: true } as T;
|
||||
if (path === "/builds") return { state: "queued" } as T;
|
||||
if (path.includes("/events"))
|
||||
return [{ type: "status", status: {
|
||||
state: "queued",
|
||||
phase: ["queued", "preparing", "waiting", "waiting"][polls],
|
||||
} }] as T;
|
||||
if (path.endsWith("/reconcile")) {
|
||||
polls++;
|
||||
await Bun.sleep(110);
|
||||
return polls < 3
|
||||
? { state: "queued" } as T
|
||||
: { state: "failed", error: "build stopped" } as T;
|
||||
}
|
||||
throw new Error(path);
|
||||
};
|
||||
await expect(provideContext(() => runUp(true, request, { trust }))).rejects.toThrow("build stopped");
|
||||
const output = rendered.join("");
|
||||
expect(polls).toBe(3);
|
||||
expect(output.match(/Build app/g)).toHaveLength(1);
|
||||
expect(output.match(/Build queued/g)).toHaveLength(1);
|
||||
expect(output).toContain("Build preparing");
|
||||
expect(output).toContain("Build waiting");
|
||||
expect(output).toContain("build stopped");
|
||||
} finally {
|
||||
writes.mockRestore();
|
||||
if (tty) Object.defineProperty(process.stdout, "isTTY", tty);
|
||||
else Reflect.deleteProperty(process.stdout, "isTTY");
|
||||
process.chdir(previousCwd);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 5_000);
|
||||
|
||||
test("attaches each build log to its started image child", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "kuber-up-api-"));
|
||||
const previousCwd = process.cwd();
|
||||
|
||||
Reference in New Issue
Block a user