actions / actions/toolkit

Silent I/O failure after getExecOutput() completes in GitHub Actions runtime

Open
#2,303 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
5.9k
Forks
1.8k
PR merge metrics
No merged PRs in 30d

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 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

  1. Subprocess executes correctly (Jest runs, tests pass)
  2. result.stdout and result.stderr are populated
  3. 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:

  1. Run an external command via @actions/exec
  2. Then process the results (log, write files, set outputs)

This is a common pattern for test runners, linters, formatters, and build tools.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the minimal TypeScript action from the issue using action.yml, Node 20, and @actions/exec on ubuntu-latest. Compare I/O before and after getExecOutput(), then inspect the execution and subprocess-completion paths; use the referenced CHANGELOG.md and 28-commit debugging history as context. Done means identifying a reproducible toolkit-side cause and adding a regression test for post-exec logging and filesystem I/O.

Written by the indexing model from the issue text.

Assessment

Tech stack
github-actions, node.js, typescript
Domain
ci-cd, devtools, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.