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 <cursoragent@cursor.com>
This commit is contained in:
Colin McDonnell
2026-02-24 15:32:32 +00:00
committed by pullfrog[bot]
parent 4f1e4a2e7a
commit cd1ea5267c
5 changed files with 61 additions and 25 deletions
+30 -14
View File
@@ -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) { "node_modules/.pnpm/undici@7.16.0/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
"use strict"; "use strict";
var { writeFile, readFile, mkdir } = __require("node:fs/promises"); 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 { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("node:timers");
var { InvalidArgumentError, UndiciError } = require_errors4(); var { InvalidArgumentError, UndiciError } = require_errors4();
var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils(); var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -56498,7 +56498,7 @@ var require_snapshot_recorder = __commonJS({
throw new InvalidArgumentError("Snapshot path is required"); throw new InvalidArgumentError("Snapshot path is required");
} }
const resolvedPath = resolve3(path3); 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]) => ({ const data = Array.from(this.#snapshots.entries()).map(([hash2, snapshot2]) => ({
hash: hash2, hash: hash2,
snapshot: snapshot2 snapshot: snapshot2
@@ -97612,6 +97612,7 @@ var require_semver2 = __commonJS({
// entry.ts // entry.ts
var core5 = __toESM(require_core(), 1); 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 // node_modules/.pnpm/@ark+util@0.56.0/node_modules/@ark/util/out/arrays.js
var liftArray = (data) => Array.isArray(data) ? data : [data]; var liftArray = (data) => Array.isArray(data) ? data : [data];
@@ -141626,7 +141627,10 @@ function DelegateTool(ctx) {
if (!ctx.toolState.selectedMode) { if (!ctx.toolState.selectedMode) {
log.info(`\xBB warning: delegating without calling select_mode first (mode=${mode})`); 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 taskEntries = params.tasks.map((task) => {
const effort = task.effort ?? "auto"; const effort = task.effort ?? "auto";
const subagent = createSubagentState({ ctx, mode, label: task.label }); const subagent = createSubagentState({ ctx, mode, label: task.label });
@@ -141646,10 +141650,12 @@ function DelegateTool(ctx) {
const results = taskEntries.map((entry, i) => { const results = taskEntries.map((entry, i) => {
const outcome = settled[i]; const outcome = settled[i];
const error49 = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error; const error49 = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
log.debug( const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error49);
`\xBB task "${entry.task.label}" result: output=${entry.subagent.output !== void 0}, status=${entry.subagent.status}` 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; const succeeded = results.filter((r) => r.success).length;
log.info(`\xBB delegation completed: ${succeeded}/${results.length} succeeded (mode=${mode})`); log.info(`\xBB delegation completed: ${succeeded}/${results.length} succeeded (mode=${mode})`);
@@ -142071,12 +142077,15 @@ var installNodeDependencies = {
const result = await spawn({ const result = await spawn({
cmd: resolved.command, cmd: resolved.command,
args: resolved.args, args: resolved.args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, 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) { if (result.exitCode !== 0) {
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
const errorMessage = output || `exited with code ${result.exitCode}`; const errorMessage = output || `exited with code ${result.exitCode}`;
return { return {
language: "node", language: "node",
@@ -142213,20 +142222,26 @@ var installPythonDependencies = {
} }
} }
const [cmd, ...args2] = config3.installCmd; 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({ const result = await spawn({
cmd, cmd,
args: args2, args: args2,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, 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) { if (result.exitCode !== 0) {
return { return {
language: "python", language: "python",
packageManager: config3.tool, packageManager: config3.tool,
configFile: config3.file, configFile: config3.file,
dependenciesInstalled: false, dependenciesInstalled: false,
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`] issues: [output || `${cmd} exited with code ${result.exitCode}`]
}; };
} }
return { return {
@@ -147697,6 +147712,7 @@ ${instructions.user}` : null,
} }
// entry.ts // entry.ts
process.env.PATH = `${dirname2(process.execPath)}:${process.env.PATH}`;
async function run() { async function run() {
try { try {
const result = await main(); const result = await main();
+6
View File
@@ -4,9 +4,15 @@
* entry point for pullfrog/pullfrog - unified action * entry point for pullfrog/pullfrog - unified action
*/ */
import { dirname } from "node:path";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { main } from "./main.ts"; 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<void> { async function run(): Promise<void> {
try { try {
const result = await main(); const result = await main();
+8 -4
View File
@@ -77,7 +77,10 @@ export function DelegateTool(ctx: ToolContext) {
} }
// matched by delegate test validators — update tests if changed // 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 taskEntries = params.tasks.map((task) => {
const effort = task.effort ?? "auto"; const effort = task.effort ?? "auto";
@@ -100,10 +103,11 @@ export function DelegateTool(ctx: ToolContext) {
const results: DelegateTaskResult[] = taskEntries.map((entry, i) => { const results: DelegateTaskResult[] = taskEntries.map((entry, i) => {
const outcome = settled[i]; const outcome = settled[i];
const error = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error; const error = outcome.status === "rejected" ? String(outcome.reason) : outcome.value.error;
log.debug( const result = buildTaskResult(entry.task.label, entry.effort, entry.subagent, error);
`» task "${entry.task.label}" result: output=${entry.subagent.output !== undefined}, status=${entry.subagent.status}` 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; const succeeded = results.filter((r) => r.success).length;
+7 -4
View File
@@ -159,13 +159,16 @@ export const installNodeDependencies: PrepDefinition = {
cmd: resolved.command, cmd: resolved.command,
args: resolved.args, args: resolved.args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, 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) { 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}`; const errorMessage = output || `exited with code ${result.exitCode}`;
return { return {
language: "node", language: "node",
+10 -3
View File
@@ -162,21 +162,28 @@ export const installPythonDependencies: PrepDefinition = {
// run the install command // run the install command
const [cmd, ...args] = config.installCmd; const [cmd, ...args] = config.installCmd;
log.info(`» running: ${cmd} ${args.join(" ")}`); const fullCommand = `${cmd} ${args.join(" ")}`;
log.info(`» running: ${fullCommand}`);
const result = await spawn({ const result = await spawn({
cmd, cmd,
args, args,
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" }, 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) { if (result.exitCode !== 0) {
return { return {
language: "python", language: "python",
packageManager: config.tool, packageManager: config.tool,
configFile: config.file, configFile: config.file,
dependenciesInstalled: false, dependenciesInstalled: false,
issues: [result.stderr || `${cmd} exited with code ${result.exitCode}`], issues: [output || `${cmd} exited with code ${result.exitCode}`],
}; };
} }