From d28baf235db491f17f9363a752470a20b514fe01 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 5 Jun 2026 18:10:50 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20fix=20workflow=20action=20ex?= =?UTF-8?q?ec=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent ctx.exec command output and descendant-cleanup warnings from polluting workflow action stdout/stderr while keeping command output available on ctx.exec results. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `614410{MUX_COSTS_USD:-0}`_ --- .mux/workflows/.scratch/.gitignore | 2 ++ .../workflows/WorkflowActionRunner.test.ts | 26 +++++++++++++++++++ .../workflows/WorkflowActionRunner.ts | 7 ++--- .../services/workflows/WorkflowRunner.test.ts | 3 +++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.mux/workflows/.scratch/.gitignore b/.mux/workflows/.scratch/.gitignore index 4af1d0cfb8..b030e71697 100644 --- a/.mux/workflows/.scratch/.gitignore +++ b/.mux/workflows/.scratch/.gitignore @@ -1,3 +1,5 @@ # mux: hide scratch workflow drafts when repo rules unignore workflows * !.gitignore +# mux: hide scratch workflow drafts when repo rules unignore workflows +* diff --git a/src/node/services/workflows/WorkflowActionRunner.test.ts b/src/node/services/workflows/WorkflowActionRunner.test.ts index 3cdb4b61f6..6355749bf8 100644 --- a/src/node/services/workflows/WorkflowActionRunner.test.ts +++ b/src/node/services/workflows/WorkflowActionRunner.test.ts @@ -202,6 +202,32 @@ describe("WorkflowActionRunner", () => { }); }); + test("keeps ctx.exec output out of action diagnostics", async () => { + using tmp = new DisposableTempDir("workflow-action-exec-diagnostics"); + const sourcePath = path.join(tmp.path, "exec-diagnostics.js"); + const source = ` + module.exports.metadata = { version: 1, description: "Exec diagnostics", effect: "read" }; + module.exports.execute = async (_input, ctx) => { + console.log("action log"); + const result = await ctx.exec(process.execPath, ["-e", "process.stdout.write('cmd stdout'); process.stderr.write('cmd stderr')"]); + return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode }; + }; + `; + await fs.writeFile(sourcePath, source, "utf-8"); + const runner = new WorkflowActionRunner(); + + const result = await runner.execute(createAction(sourcePath, source), { + input: null, + cwd: tmp.path, + timeoutMs: 10_000, + artifactDir: path.join(tmp.path, "artifacts"), + }); + + expect(result.output).toEqual({ stdout: "cmd stdout", stderr: "cmd stderr", exitCode: 0 }); + expect(result.stdout).toBe("action log\n"); + expect(result.stderr).toBe(""); + }); + test("built-in git actions reject truncated command output", async () => { using tmp = new DisposableTempDir("workflow-action-git-truncated"); const repoRoot = path.join(tmp.path, "repo"); diff --git a/src/node/services/workflows/WorkflowActionRunner.ts b/src/node/services/workflows/WorkflowActionRunner.ts index 34925724e2..d32d176169 100644 --- a/src/node/services/workflows/WorkflowActionRunner.ts +++ b/src/node/services/workflows/WorkflowActionRunner.ts @@ -992,7 +992,10 @@ function captureResult(capture) { function listChildPids(pid) { try { - return execFileSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf-8" }) + return execFileSync("ps", ["-axo", "pid=,ppid="], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }) .trim() .split(/\n+/) .map((line) => line.trim().split(/\s+/).map((value) => Number(value))) @@ -1328,11 +1331,9 @@ async function execCommand(command, args = [], options = {}) { timer?.unref?.(); child.stdout?.on("data", (chunk) => { appendCapture(stdout, chunk); - process.stdout.write(chunk); }); child.stderr?.on("data", (chunk) => { appendCapture(stderr, chunk); - process.stderr.write(chunk); }); child.on("exit", (code, childSignal) => { exitCode = code; diff --git a/src/node/services/workflows/WorkflowRunner.test.ts b/src/node/services/workflows/WorkflowRunner.test.ts index 24215f9d00..4a07caa09e 100644 --- a/src/node/services/workflows/WorkflowRunner.test.ts +++ b/src/node/services/workflows/WorkflowRunner.test.ts @@ -1806,6 +1806,9 @@ describe("WorkflowRunner", () => { expect(parsed.ignored).toContain("ignored.txt"); expect(parsed.branchFiles).toContain("feature.txt"); expect(parsed.branchDiff).toContain("feature.txt"); + expect(parsed.branchDiff).toContain("diff --git"); + expect(parsed.branchDiff).toContain("+feature"); + expect(parsed.branchStat).not.toContain("diff --git"); expect(parsed.unstagedDiff).toContain("+dirty"); expect(parsed.diffTruncated).toEqual({ branch: false, staged: false, unstaged: false }); expect(parsed.branchStat).toContain("feature.txt"); From 44931448117c08c4023bdcb9c549b5f8cc7c92d8 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 5 Jun 2026 18:28:06 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20test=20ps=20cleanup=20diagno?= =?UTF-8?q?stics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add deterministic coverage for noisy ps cleanup and remove the accidental duplicate scratch workflow ignore rule found by deep review. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `699312{MUX_COSTS_USD:-0}`_ --- .mux/workflows/.scratch/.gitignore | 2 -- .../workflows/WorkflowActionRunner.test.ts | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.mux/workflows/.scratch/.gitignore b/.mux/workflows/.scratch/.gitignore index b030e71697..4af1d0cfb8 100644 --- a/.mux/workflows/.scratch/.gitignore +++ b/.mux/workflows/.scratch/.gitignore @@ -1,5 +1,3 @@ # mux: hide scratch workflow drafts when repo rules unignore workflows * !.gitignore -# mux: hide scratch workflow drafts when repo rules unignore workflows -* diff --git a/src/node/services/workflows/WorkflowActionRunner.test.ts b/src/node/services/workflows/WorkflowActionRunner.test.ts index 6355749bf8..afdcafb721 100644 --- a/src/node/services/workflows/WorkflowActionRunner.test.ts +++ b/src/node/services/workflows/WorkflowActionRunner.test.ts @@ -228,6 +228,41 @@ describe("WorkflowActionRunner", () => { expect(result.stderr).toBe(""); }); + test("keeps cleanup ps warnings out of action diagnostics", async () => { + if (process.platform === "win32") { + return; + } + using tmp = new DisposableTempDir("workflow-action-ps-diagnostics"); + const fakeBinDir = path.join(tmp.path, "bin"); + const fakePsPath = path.join(fakeBinDir, "ps"); + const sourcePath = path.join(tmp.path, "ps-diagnostics.js"); + const sentinel = "fake ps warning should stay hidden"; + await fs.mkdir(fakeBinDir, { recursive: true }); + await fs.writeFile(fakePsPath, `#!/bin/sh\nprintf '${sentinel}\\n' >&2\n`, "utf-8"); + await fs.chmod(fakePsPath, 0o755); + const source = ` + module.exports.metadata = { version: 1, description: "Ps diagnostics", effect: "read" }; + module.exports.execute = async (_input, ctx) => { + process.env.PATH = ${JSON.stringify(fakeBinDir + path.delimiter)} + (process.env.PATH || ""); + const result = await ctx.exec(process.execPath, ["-e", ""]); + return { exitCode: result.exitCode }; + }; + `; + await fs.writeFile(sourcePath, source, "utf-8"); + const runner = new WorkflowActionRunner(); + + const result = await runner.execute(createAction(sourcePath, source), { + input: null, + cwd: tmp.path, + timeoutMs: 10_000, + artifactDir: path.join(tmp.path, "artifacts"), + }); + + expect(result.output).toEqual({ exitCode: 0 }); + expect(result.stderr).not.toContain(sentinel); + expect(result.stderr).toBe(""); + }); + test("built-in git actions reject truncated command output", async () => { using tmp = new DisposableTempDir("workflow-action-git-truncated"); const repoRoot = path.join(tmp.path, "repo");