-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
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 logexec.getExecOutput(): runs correctly, returns populated resultcore.info()after exec: silently lostfs.writeFileSync()after exec: file never created- No errors thrown. Exit code 0.
Observed Behavior
- Subprocess executes correctly (Jest runs, tests pass)
result.stdoutandresult.stderrare populated- All I/O after the exec call fails silently:
console.log- silentconsole.error- silentcore.info- silentcore.debug- silentfs.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.txtThis "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:
- Repository: https://github.com/Wolfe-Jam/faf-taf-git
- Versions v1.0.0 through v1.2.2 (debugging) → v2.0.0 (workaround)
- CHANGELOG documents every attempt: https://github.com/Wolfe-Jam/faf-taf-git/blob/main/CHANGELOG.md
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.