Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/exec/__tests__/exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const SPAWN_WAIT_SCRIPT = path.join(
'scripts',
'spawn-wait-for-file.cjs'
)
const SELF_TERMINATE_SCRIPT = path.join(
__dirname,
'scripts',
'self-terminate.cjs'
)

let outstream: stream.Writable
let errstream: stream.Writable
Expand Down Expand Up @@ -192,6 +197,24 @@ describe('@actions/exec', () => {
}
})

it('Fails when process terminates via signal', async () => {
if (IS_WINDOWS) {
return
}

const nodePath: string = await io.which('node', true)
const _testExecOptions = getExecOptions()

await exec
.exec(`"${nodePath}"`, [SELF_TERMINATE_SCRIPT], _testExecOptions)
.then(() => {
throw new Error('Should not have succeeded')
})
.catch(err => {
expect(err.message).toContain('terminated by signal SIGTERM')
})
})

it('Succeeds on stderr by default', async () => {
const scriptPath: string = path.join(
__dirname,
Expand Down
3 changes: 3 additions & 0 deletions packages/exec/__tests__/scripts/self-terminate.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
setTimeout(() => {
process.kill(process.pid, 'SIGTERM')
}, 50)
19 changes: 15 additions & 4 deletions packages/exec/src/toolrunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,15 +501,21 @@ export class ToolRunner extends events.EventEmitter {
state.CheckComplete()
})

cp.on('exit', (code: number) => {
cp.on('exit', (code: number | null, signal: NodeJS.Signals | null) => {
state.processExitCode = code
state.processSignal = signal
state.processExited = true
this._debug(`Exit code ${code} received from tool '${this.toolPath}'`)
if (signal) {
this._debug(`Signal ${signal} received from tool '${this.toolPath}'`)
} else {
this._debug(`Exit code ${code} received from tool '${this.toolPath}'`)
}
state.CheckComplete()
})

cp.on('close', (code: number) => {
cp.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
state.processExitCode = code
state.processSignal = signal
state.processExited = true
state.processClosed = true
this._debug(`STDIO streams have closed for tool '${this.toolPath}'`)
Expand Down Expand Up @@ -625,7 +631,8 @@ class ExecState extends events.EventEmitter {

processClosed = false // tracks whether the process has exited and stdio is closed
processError = ''
processExitCode = 0
processExitCode: number | null = 0
processSignal: NodeJS.Signals | null = null
processExited = false // tracks whether the process has exited
processStderr = false // tracks whether stderr was written to
private delay = 10000 // 10 seconds
Expand Down Expand Up @@ -658,6 +665,10 @@ class ExecState extends events.EventEmitter {
error = new Error(
`There was an error when attempting to execute the process '${this.toolPath}'. This may indicate the process failed to start. Error: ${this.processError}`
)
} else if (this.processSignal) {
error = new Error(
`The process '${this.toolPath}' failed due to signal ${this.processSignal}`
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new signal-termination error message text ("failed due to signal") doesn't match the newly added test expectation ("terminated by signal"). As-is, the test will fail; consider adjusting this message to include the wording the test asserts (or update the test to match the intended message).

Suggested change
`The process '${this.toolPath}' failed due to signal ${this.processSignal}`
`The process '${this.toolPath}' terminated by signal ${this.processSignal}`

Copilot uses AI. Check for mistakes.
)
} else if (this.processExitCode !== 0 && !this.options.ignoreReturnCode) {
Comment on lines +668 to 672
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signal-based termination currently throws regardless of options.ignoreReturnCode. Since ignoreReturnCode is documented as "will not fail leaving it up to the caller" (interfaces.ts), this changes behavior for callers that intentionally ignore failures. Consider honoring ignoreReturnCode for the signal case as well (or introducing/ documenting a separate option for ignoring signals).

Copilot uses AI. Check for mistakes.
error = new Error(
`The process '${this.toolPath}' failed with exit code ${this.processExitCode}`
Expand Down
Loading