Description
After @actions/exec.getExecOutput() completes in a GitHub Actions Node.js action, all subsequent I/O operations (logging, filesystem writes) fail silently. No errors are thrown. The process continues executing but cannot produce any output.
This behavior does not occur in local Node.js execution. It is specific to the GitHub Actions runtime environment.
Reproduction
Minimal action code:
import * as exec from '@actions/exec';
import * as core from '@actions/core';
import * as fs from 'fs';
// This logs successfully
core.info('Before exec - this appears in the log');
const result = await exec.getExecOutput('npm', ['test'], {
ignoreReturnCode: true
});
// This never appears in the log
core.info('After exec - this is silently lost');
// This file is never created
fs.writeFileSync('debug.txt', `Exit code: ${result.exitCode}`);
// result.stdout and result.stderr ARE populated correctly
// The subprocess ran fine. The calling context is broken.
What happens:
core.info() before exec: appears in log
exec.getExecOutput(): runs correctly, returns populated result
core.info() after exec: silently lost
fs.writeFileSync() after exec: file never created
- No errors thrown. Exit code 0.
Observed Behavior
- Subprocess executes correctly (Jest runs, tests pass)
result.stdout and result.stderr are populated
- All I/O after the exec call fails silently:
console.log - silent
console.error - silent
core.info - silent
core.debug - silent
fs.writeFileSync - file not created, no error thrown
Expected Behavior
Logging and filesystem operations should work normally after getExecOutput() returns.
Debugging Steps Taken
We tested 19 different methods across 16 versions to isolate the issue:
| Method |
Result |
console.log after exec |
Silent |
console.error after exec |
Silent |
core.info after exec |
Silent |
Dual logging (core.info + console.log) |
Both silent |
fs.writeFileSync to working directory |
File not created |
fs.writeFileSync to process.cwd() |
File not created |
fs.writeFileSync to os.tmpdir() |
File not created |
fs.writeFileSync to 4 locations simultaneously |
No files found anywhere |
| Unconditional logs (no if/else) |
Silent |
| CHECKPOINT logs before and after exec |
Before: works. After: silent. |
Fix this binding with arrow functions |
Not the issue |
Switch exec.exec() to exec.getExecOutput() |
Same failure |
Switch child_process.spawn to @actions/exec |
Same failure |
Key finding (CHECKPOINT test): Placing core.info('CHECKPOINT 1') before the exec call and core.info('CHECKPOINT 2') after revealed that CHECKPOINT 1 appears in the log and CHECKPOINT 2 does not. The execution context becomes corrupted at the point of subprocess return.
Workaround
Separate test execution from result processing. Run the subprocess in a prior workflow step and read the output file:
# Step 1: Run in normal shell (I/O works fine)
- run: npm test 2>&1 | tee test-output.txt
# Step 2: Action reads file (no subprocess needed)
- uses: my-action@v2
with:
test-output-file: test-output.txt
This "pre-capture pattern" avoids the issue entirely by not spawning subprocesses inside the Node.js action.
Environment
- Runner:
ubuntu-latest
- Node: 20 (via
runs.using: 'node20' in action.yml)
- @actions/exec: ^1.1.1
- @actions/core: ^1.10.1
- Subprocess:
npm test (Jest)
Evidence
The complete debugging journey is documented across 28 commits:
Impact
This affects any GitHub Action that needs to:
- Run an external command via
@actions/exec
- Then process the results (log, write files, set outputs)
This is a common pattern for test runners, linters, formatters, and build tools.
Description
After
@actions/exec.getExecOutput()completes in a GitHub Actions Node.js action, all subsequent I/O operations (logging, filesystem writes) fail silently. No errors are thrown. The process continues executing but cannot produce any output.This behavior does not occur in local Node.js execution. It is specific to the GitHub Actions runtime environment.
Reproduction
Minimal action code:
What happens:
core.info()before exec: appears in logexec.getExecOutput(): runs correctly, returns populated resultcore.info()after exec: silently lostfs.writeFileSync()after exec: file never createdObserved Behavior
result.stdoutandresult.stderrare populatedconsole.log- silentconsole.error- silentcore.info- silentcore.debug- silentfs.writeFileSync- file not created, no error thrownExpected Behavior
Logging and filesystem operations should work normally after
getExecOutput()returns.Debugging Steps Taken
We tested 19 different methods across 16 versions to isolate the issue:
console.logafter execconsole.errorafter execcore.infoafter execcore.info+console.log)fs.writeFileSyncto working directoryfs.writeFileSynctoprocess.cwd()fs.writeFileSynctoos.tmpdir()fs.writeFileSyncto 4 locations simultaneouslythisbinding with arrow functionsexec.exec()toexec.getExecOutput()child_process.spawnto@actions/execKey finding (CHECKPOINT test): Placing
core.info('CHECKPOINT 1')before the exec call andcore.info('CHECKPOINT 2')after revealed that CHECKPOINT 1 appears in the log and CHECKPOINT 2 does not. The execution context becomes corrupted at the point of subprocess return.Workaround
Separate test execution from result processing. Run the subprocess in a prior workflow step and read the output file:
This "pre-capture pattern" avoids the issue entirely by not spawning subprocesses inside the Node.js action.
Environment
ubuntu-latestruns.using: 'node20'in action.yml)npm test(Jest)Evidence
The complete debugging journey is documented across 28 commits:
Impact
This affects any GitHub Action that needs to:
@actions/execThis is a common pattern for test runners, linters, formatters, and build tools.