From cd1ea5267ca56442946d6a02a0b1c0ec6deeed77 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Tue, 24 Feb 2026 15:32:32 +0000 Subject: [PATCH] fix node24 PATH propagation and improve action logging (#381) Add node24 binary directory to PATH in action entry point so spawned processes (pnpm, npm, etc.) resolve to the correct node version instead of the runner's default v20. Improve delegate task result logging with success/failure status and summaries. Use collapsible log groups for dependency install output instead of raw streaming. Co-authored-by: Cursor --- entry | 44 +++++++++++++++++++++---------- entry.ts | 6 +++++ mcp/delegate.ts | 12 ++++++--- prep/installNodeDependencies.ts | 11 +++++--- prep/installPythonDependencies.ts | 13 ++++++--- 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/entry b/entry index 4079762..264c92e 100755 --- a/entry +++ b/entry @@ -56274,7 +56274,7 @@ var require_snapshot_recorder = __commonJS({ "node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) { "use strict"; var { writeFile, readFile, mkdir } = __require("node:fs/promises"); - var { dirname: dirname2, resolve: resolve3 } = __require("node:path"); + var { dirname: dirname3, resolve: resolve3 } = __require("node:path"); var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers"); var { InvalidArgumentError, UndiciError } = require_errors4(); var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils(); @@ -56498,7 +56498,7 @@ var require_snapshot_recorder = __commonJS({ throw new InvalidArgumentError("Snapshot path is required"); } const resolvedPath = resolve3(path3); - await mkdir(dirname2(resolvedPath), { recursive: true }); + await mkdir(dirname3(resolvedPath), { recursive: true }); const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({ hash: hash2, snapshot: snapshot2 @@ -97612,6 +97612,7 @@ var require_semver2 = __commonJS({ // entry.ts var core5 = __toESM(require_core(), 1); +import { dirname as dirname2 } from "node:path"; // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js var liftArray = (data) => Array.isArray(data) ? data : [data]; @@ -141626,7 +141627,10 @@ function DelegateTool(ctx) { if (!ctx.toolState.selectedMode) { log.info(`\xBB warning: delegating without calling select_mode first (mode=${mode})`); } - log.info(`\xBB delegating ${params.tasks.length} task(s) in parallel (mode=${mode})`); + const n = params.tasks.length; + log.info( + `\xBB delegating ${n} task${n === 1 ? "" : "s"}${n > 1 ? " in parallel" : ""} (mode=${mode})` + ); const taskEntries = params.tasks.map((task) => { const effort = task.effort ?? "auto"; const subagent = createSubagentState({ ctx, mode, label: task.label }); @@ -141646,10 +141650,12 @@ function DelegateTool(ctx) { const results = taskEntries.map((entry, i) => { const outcome = settled[i]; const error49 = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error; - log.debug( - `\xBB task "${entry.task.label}" result: output=${entry.subagent.output !== void 0}, status=${entry.subagent.status}` + const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49); + log.info( + `\xBB task "${entry.task.label}" ${result.success ? "succeeded" : "failed"}: +${result.summary}` ); - return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49); + return result; }); const succeeded = results.filter((r) => r.success).length; log.info(`\xBB delegation completed: ${succeeded}/${results.length} succeeded (mode=${mode})`); @@ -142071,12 +142077,15 @@ var installNodeDependencies = { const result = await spawn({ cmd: resolved.command, args: resolved.args, - env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, - onStdout: (chunk) => process.stdout.write(chunk), - onStderr: (chunk) => process.stderr.write(chunk) + env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" } }); + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + if (output) { + log.startGroup(`${fullCommand} output`); + log.info(output); + log.endGroup(); + } if (result.exitCode !== 0) { - const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); const errorMessage = output || `exited with code ${result.exitCode}`; return { language: "node", @@ -142213,20 +142222,26 @@ var installPythonDependencies = { } } const [cmd, ...args2] = config3.installCmd; - log.info(`\xBB running: ${cmd} ${args2.join(" ")}`); + const fullCommand = `${cmd} ${args2.join(" ")}`; + log.info(`\xBB running: ${fullCommand}`); const result = await spawn({ cmd, args: args2, - env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, - onStderr: (chunk) => process.stderr.write(chunk) + env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" } }); + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + if (output) { + log.startGroup(`${fullCommand} output`); + log.info(output); + log.endGroup(); + } if (result.exitCode !== 0) { return { language: "python", packageManager: config3.tool, configFile: config3.file, dependenciesInstalled: false, - issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`] + issues: [output || `${cmd} exited with code ${result.exitCode}`] }; } return { @@ -147697,6 +147712,7 @@ ${instructions.user}` : null, } // entry.ts +process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`; async function run() { try { const result = await main(); diff --git a/entry.ts b/entry.ts index ef89d70..a89ae37 100644 --- a/entry.ts +++ b/entry.ts @@ -4,9 +4,15 @@ * entry point for pullfrog/pullfrog - unified action */ +import { dirname } from "node:path"; import * as core from "@actions/core"; import { main } from "./main.ts"; +// GitHub Actions runs the action entry point with the node24 binary specified +// in action.yml, but doesn't add that binary's directory to PATH. Without this, +// spawned processes (pnpm, npm, etc.) resolve to the runner's default node (v20). +process.env.PATH = `${dirname(process.execPath)}:${process.env.PATH}`; + async function run(): Promise { try { const result = await main(); diff --git a/mcp/delegate.ts b/mcp/delegate.ts index 6371e85..b725b78 100644 --- a/mcp/delegate.ts +++ b/mcp/delegate.ts @@ -77,7 +77,10 @@ export function DelegateTool(ctx: ToolContext) { } // matched by delegate test validators — update tests if changed - log.info(`» delegating ${params.tasks.length} task(s) in parallel (mode=${mode})`); + const n = params.tasks.length; + log.info( + `» delegating ${n} task${n === 1 ? "" : "s"}${n > 1 ? " in parallel" : ""} (mode=${mode})` + ); const taskEntries = params.tasks.map((task) => { const effort = task.effort ?? "auto"; @@ -100,10 +103,11 @@ export function DelegateTool(ctx: ToolContext) { const results: DelegateTaskResult[] = taskEntries.map((entry, i) => { const outcome = settled[i]; const error = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error; - log.debug( - `» task "${entry.task.label}" result: output=${entry.subagent.output !== undefined}, status=${entry.subagent.status}` + const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error); + log.info( + `» task "${entry.task.label}" ${result.success ? "succeeded" : "failed"}:\n${result.summary}` ); - return buildTaskResult(entry.task.label, entry.effort, entry.subagent, error); + return result; }); const succeeded = results.filter((r) => r.success).length; diff --git a/prep/installNodeDependencies.ts b/prep/installNodeDependencies.ts index 15f1e21..d315217 100644 --- a/prep/installNodeDependencies.ts +++ b/prep/installNodeDependencies.ts @@ -159,13 +159,16 @@ export const installNodeDependencies: PrepDefinition = { cmd: resolved.command, args: resolved.args, env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, - onStdout: (chunk) => process.stdout.write(chunk), - onStderr: (chunk) => process.stderr.write(chunk), }); + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + if (output) { + log.startGroup(`${fullCommand} output`); + log.info(output); + log.endGroup(); + } + if (result.exitCode !== 0) { - // combine stdout and stderr for better error context (pnpm often outputs errors to stdout) - const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); const errorMessage = output || `exited with code ${result.exitCode}`; return { language: "node", diff --git a/prep/installPythonDependencies.ts b/prep/installPythonDependencies.ts index 0892115..b8dcc57 100644 --- a/prep/installPythonDependencies.ts +++ b/prep/installPythonDependencies.ts @@ -162,21 +162,28 @@ export const installPythonDependencies: PrepDefinition = { // run the install command const [cmd, ...args] = config.installCmd; - log.info(`» running: ${cmd} ${args.join(" ")}`); + const fullCommand = `${cmd} ${args.join(" ")}`; + log.info(`» running: ${fullCommand}`); const result = await spawn({ cmd, args, env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, - onStderr: (chunk) => process.stderr.write(chunk), }); + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + if (output) { + log.startGroup(`${fullCommand} output`); + log.info(output); + log.endGroup(); + } + if (result.exitCode !== 0) { return { language: "python", packageManager: config.tool, configFile: config.file, dependenciesInstalled: false, - issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`], + issues: [output || `${cmd} exited with code ${result.exitCode}`], }; }