From e899f42009de266b8757823f858d0cc35a1c9aec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:12:49 +0000 Subject: [PATCH 1/2] Initial plan From 5f0925612dc5abda8b027a6c4a4ff345bc5f1bba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:01:13 +0000 Subject: [PATCH 2/2] fix: copy node binary to standard path for AWF sandbox on custom runners On custom image runners like aw-gpu-runner-T4, actions/setup-node installs Node.js into the toolcache (e.g., /home/runner/work/_tool/node/...) rather than a standard system path. AWF's chroot container may not have access to these toolcache paths, causing `node: command not found` errors when the Copilot driver script runs inside the sandbox. Add a new step after actions/setup-node that copies the node binary to /usr/local/bin/node - a standard system path that is always accessible inside AWF's chroot container. This follows the same pattern as the existing GOROOT capture step for Go's AWF chroot compatibility. The fix only triggers for custom image runners (where node runtime detection adds actions/setup-node), not for standard GitHub-hosted runners (ubuntu-*, windows-*) that have node pre-installed at system paths. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d0ff596d-7a09-42e9-9bc5-ba25414271b8 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/ci-coach.lock.yml | 10 +++++ .../workflows/copilot-token-audit.lock.yml | 10 +++++ .../copilot-token-optimizer.lock.yml | 10 +++++ .github/workflows/daily-fact.lock.yml | 10 +++++ .github/workflows/daily-hippo-learn.lock.yml | 10 +++++ .../workflows/daily-issues-report.lock.yml | 10 +++++ .../daily-multi-device-docs-tester.lock.yml | 10 +++++ .github/workflows/daily-news.lock.yml | 10 +++++ .github/workflows/docs-noob-tester.lock.yml | 10 +++++ .github/workflows/go-logger.lock.yml | 10 +++++ .github/workflows/hourly-ci-cleaner.lock.yml | 10 +++++ .github/workflows/jsweep.lock.yml | 10 +++++ .github/workflows/mcp-inspector.lock.yml | 10 +++++ .github/workflows/smoke-test-tools.lock.yml | 10 +++++ .../workflows/technical-doc-writer.lock.yml | 10 +++++ .github/workflows/unbloat-docs.lock.yml | 10 +++++ pkg/workflow/runtime_setup_test.go | 15 ++++--- pkg/workflow/runtime_step_generator.go | 39 ++++++++++++++++++- 18 files changed, 207 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-coach.lock.yml b/.github/workflows/ci-coach.lock.yml index 5938abfb7d1..b51950a0e3f 100644 --- a/.github/workflows/ci-coach.lock.yml +++ b/.github/workflows/ci-coach.lock.yml @@ -359,6 +359,16 @@ jobs: cache: 'npm' cache-dependency-path: 'actions/setup/js/package-lock.json' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/copilot-token-audit.lock.yml b/.github/workflows/copilot-token-audit.lock.yml index f602b947022..baa5bb9247d 100644 --- a/.github/workflows/copilot-token-audit.lock.yml +++ b/.github/workflows/copilot-token-audit.lock.yml @@ -408,6 +408,16 @@ jobs: cache: 'npm' cache-dependency-path: 'actions/setup/js/package-lock.json' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/copilot-token-optimizer.lock.yml b/.github/workflows/copilot-token-optimizer.lock.yml index c89a101b30d..b72c3989c34 100644 --- a/.github/workflows/copilot-token-optimizer.lock.yml +++ b/.github/workflows/copilot-token-optimizer.lock.yml @@ -361,6 +361,16 @@ jobs: cache: 'npm' cache-dependency-path: 'actions/setup/js/package-lock.json' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/daily-fact.lock.yml b/.github/workflows/daily-fact.lock.yml index fc76a76d58d..4f15965e89d 100644 --- a/.github/workflows/daily-fact.lock.yml +++ b/.github/workflows/daily-fact.lock.yml @@ -455,6 +455,16 @@ jobs: with: node-version: '24' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/daily-hippo-learn.lock.yml b/.github/workflows/daily-hippo-learn.lock.yml index 0f818149e45..be156cb1bf8 100644 --- a/.github/workflows/daily-hippo-learn.lock.yml +++ b/.github/workflows/daily-hippo-learn.lock.yml @@ -345,6 +345,16 @@ jobs: with: node-version: '22' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml index 0da4862d2a2..7980e7d1dee 100644 --- a/.github/workflows/daily-issues-report.lock.yml +++ b/.github/workflows/daily-issues-report.lock.yml @@ -383,6 +383,16 @@ jobs: with: node-version: '24' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index 17eb321e8b4..d987f2d5fd5 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -362,6 +362,16 @@ jobs: with: node-version: '24' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index 114edd30501..c745449c7b5 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -381,6 +381,16 @@ jobs: with: node-version: '24' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index f728d8db99a..fda2be09702 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -341,6 +341,16 @@ jobs: with: node-version: '22' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index 470080bb1e6..672aadf68a5 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -349,6 +349,16 @@ jobs: cache: 'npm' cache-dependency-path: 'actions/setup/js/package-lock.json' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/hourly-ci-cleaner.lock.yml b/.github/workflows/hourly-ci-cleaner.lock.yml index 2c237c6e164..6db7c36557e 100644 --- a/.github/workflows/hourly-ci-cleaner.lock.yml +++ b/.github/workflows/hourly-ci-cleaner.lock.yml @@ -365,6 +365,16 @@ jobs: cache: 'npm' cache-dependency-path: 'actions/setup/js/package-lock.json' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/jsweep.lock.yml b/.github/workflows/jsweep.lock.yml index e9e4c1f7710..2b8eb618357 100644 --- a/.github/workflows/jsweep.lock.yml +++ b/.github/workflows/jsweep.lock.yml @@ -385,6 +385,16 @@ jobs: with: node-version: '20' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 64b347db35f..4b20f3b06e6 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -469,6 +469,16 @@ jobs: with: node-version: '24' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/smoke-test-tools.lock.yml b/.github/workflows/smoke-test-tools.lock.yml index 9dd9745a6c5..eae4c66b220 100644 --- a/.github/workflows/smoke-test-tools.lock.yml +++ b/.github/workflows/smoke-test-tools.lock.yml @@ -405,6 +405,16 @@ jobs: with: node-version: '20' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 11af5ec7571..cdbec36bc9c 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -382,6 +382,16 @@ jobs: cache: 'npm' cache-dependency-path: 'docs/package-lock.json' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 13e4b99888f..b3ae5635ea3 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -414,6 +414,16 @@ jobs: cache: 'npm' cache-dependency-path: 'docs/package-lock.json' package-manager-cache: false + - name: Copy node to standard system path for AWF sandbox + run: | + # AWF chroot container may not include the toolcache path where + # actions/setup-node installs Node.js on custom image runners. + # Copy to /usr/local/bin/node to ensure it's accessible inside AWF. + NODE_BIN="$(command -v node 2>/dev/null || true)" + if [ -n "$NODE_BIN" ] && [ "$NODE_BIN" != "/usr/local/bin/node" ]; then + sudo cp -f "$NODE_BIN" /usr/local/bin/node + sudo chmod +x /usr/local/bin/node + fi - name: Install dependencies run: npm ci working-directory: ./docs diff --git a/pkg/workflow/runtime_setup_test.go b/pkg/workflow/runtime_setup_test.go index f77faf12088..b40fdf31317 100644 --- a/pkg/workflow/runtime_setup_test.go +++ b/pkg/workflow/runtime_setup_test.go @@ -296,11 +296,12 @@ func TestGenerateRuntimeSetupSteps(t *testing.T) { requirements: []RuntimeRequirement{ {Runtime: findRuntimeByID("node"), Version: "20"}, }, - expectSteps: 1, + expectSteps: 2, // setup + copy to standard system path for AWF chroot mode checkContent: []string{ "Setup Node.js", "actions/setup-node@", "node-version: '20'", + "Copy node to standard system path for AWF sandbox", }, }, { @@ -381,10 +382,11 @@ func TestGenerateRuntimeSetupSteps(t *testing.T) { {Runtime: findRuntimeByID("node"), Version: "24"}, {Runtime: findRuntimeByID("python"), Version: "3.12"}, }, - expectSteps: 2, + expectSteps: 3, // node setup + node system path + python setup checkContent: []string{ "Setup Node.js", "Setup Python", + "Copy node to standard system path for AWF sandbox", }, }, { @@ -392,9 +394,10 @@ func TestGenerateRuntimeSetupSteps(t *testing.T) { requirements: []RuntimeRequirement{ {Runtime: findRuntimeByID("node"), Version: ""}, }, - expectSteps: 1, + expectSteps: 2, // setup + copy to standard system path for AWF chroot mode checkContent: []string{ "node-version: '24'", + "Copy node to standard system path for AWF sandbox", }, }, { @@ -818,12 +821,13 @@ func TestGenerateRuntimeSetupStepsWithIfCondition(t *testing.T) { IfCondition: "hashFiles('package.json') != ''", }, }, - expectSteps: 1, + expectSteps: 2, // setup + copy to standard system path for AWF chroot mode checkContent: []string{ "Setup Node.js", "actions/setup-node@", "node-version: '20'", "if: hashFiles('package.json') != ''", + "Copy node to standard system path for AWF sandbox", }, }, { @@ -845,7 +849,7 @@ func TestGenerateRuntimeSetupStepsWithIfCondition(t *testing.T) { IfCondition: "hashFiles('package.json') != ''", }, }, - expectSteps: 4, // go setup + GOROOT capture + python setup + node setup + expectSteps: 5, // go setup + GOROOT capture + python setup + node setup + node copy checkContent: []string{ "Setup Go", "if: hashFiles('go.mod') != ''", @@ -853,6 +857,7 @@ func TestGenerateRuntimeSetupStepsWithIfCondition(t *testing.T) { "if: hashFiles('requirements.txt') != ''", "Setup Node.js", "if: hashFiles('package.json') != ''", + "Copy node to standard system path for AWF sandbox", }, }, } diff --git a/pkg/workflow/runtime_step_generator.go b/pkg/workflow/runtime_step_generator.go index eace9904787..52b98884bba 100644 --- a/pkg/workflow/runtime_step_generator.go +++ b/pkg/workflow/runtime_step_generator.go @@ -20,7 +20,7 @@ func GenerateRuntimeSetupSteps(requirements []RuntimeRequirement) []GitHubAction steps = append(steps, generateSetupStep(&req)) // Add environment variable capture steps after setup actions for AWF chroot mode. - // Most env vars are inherited via AWF_HOST_PATH, but Go is special. + // Most env vars are inherited via AWF_HOST_PATH, but some runtimes need extra work. switch req.Runtime.ID { case "go": // GitHub Actions uses "trimmed" Go binaries that require GOROOT to be explicitly set. @@ -29,8 +29,19 @@ func GenerateRuntimeSetupSteps(requirements []RuntimeRequirement) []GitHubAction // environment, so we must capture it explicitly. runtimeStepGeneratorLog.Print("Adding GOROOT capture step for chroot mode compatibility") steps = append(steps, generateEnvCaptureStep("GOROOT", "go env GOROOT")) + case "node": + // actions/setup-node installs Node.js into the toolcache directory + // (e.g., /home/runner/work/_tool/node/24.x.x/x64/bin/node) on custom image + // runners. AWF's chroot container may not have access to these toolcache paths + // on certain runner types (e.g., aw-gpu-runner-T4 GPU runners), causing + // `node: command not found` errors inside the AWF sandbox even though PATH is + // set correctly. Copy the node binary to /usr/local/bin/node — a standard + // system path that is always accessible inside AWF's chroot — to ensure that + // the Copilot driver script and other node-dependent scripts can execute. + runtimeStepGeneratorLog.Print("Adding node system path step for AWF chroot mode compatibility") + steps = append(steps, generateNodeSystemPathStep()) } - // Note: Java and .NET don't need capture steps anymore because: + // Note: Java and .NET don't need capture steps because: // - AWF_HOST_PATH captures the complete host PATH including $JAVA_HOME/bin and $DOTNET_ROOT // - AWF's entrypoint.sh exports PATH="${AWF_HOST_PATH}" which preserves all setup-* additions } @@ -49,6 +60,30 @@ func generateEnvCaptureStep(envVar string, captureCmd string) GitHubActionStep { } } +// generateNodeSystemPathStep creates a step that copies the node binary to a standard +// system path (/usr/local/bin/node) after actions/setup-node installs it. +// +// On custom image runners (e.g., aw-gpu-runner-T4 GPU runners), actions/setup-node +// installs Node.js in the toolcache (e.g., /home/runner/work/_tool/node/24.x.x/x64/bin/). +// AWF's chroot container may not have access to these toolcache paths, causing +// `node: command not found` when the Copilot driver script runs inside the sandbox. +// Copying to /usr/local/bin/node ensures the binary is accessible at a well-known +// system path that is always available inside AWF's chroot container. +func generateNodeSystemPathStep() GitHubActionStep { + return GitHubActionStep{ + " - name: Copy node to standard system path for AWF sandbox", + " run: |", + " # AWF chroot container may not include the toolcache path where", + " # actions/setup-node installs Node.js on custom image runners.", + " # Copy to /usr/local/bin/node to ensure it's accessible inside AWF.", + " NODE_BIN=\"$(command -v node 2>/dev/null || true)\"", + " if [ -n \"$NODE_BIN\" ] && [ \"$NODE_BIN\" != \"/usr/local/bin/node\" ]; then", + " sudo cp -f \"$NODE_BIN\" /usr/local/bin/node", + " sudo chmod +x /usr/local/bin/node", + " fi", + } +} + // generateSetupStep creates a setup step for a given runtime requirement func generateSetupStep(req *RuntimeRequirement) GitHubActionStep { runtime := req.Runtime