From c74c7fc4ed021bbbbca18ccb61d6d73b9c52ed08 Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Tue, 18 Nov 2025 14:21:47 +0000 Subject: [PATCH 1/5] update hash files test --- __tests__/exec.test.ts | 71 ++++-------------------- __tests__/scripts/spawn-wait-for-file.js | 39 +++++++++++++ __tests__/scripts/stdoutputspecial.js | 4 +- 3 files changed, 52 insertions(+), 62 deletions(-) create mode 100644 __tests__/scripts/spawn-wait-for-file.js diff --git a/__tests__/exec.test.ts b/__tests__/exec.test.ts index 1f83ce4..aff2ceb 100644 --- a/__tests__/exec.test.ts +++ b/__tests__/exec.test.ts @@ -11,6 +11,11 @@ import * as io from '@actions/io' /* eslint-disable @typescript-eslint/unbound-method */ const IS_WINDOWS = process.platform === 'win32' +const SPAWN_WAIT_FOR_FILE = path.join( + __dirname, + 'scripts', + 'spawn-wait-for-file.js' +) let outstream: stream.Writable let errstream: stream.Writable @@ -365,35 +370,18 @@ describe('@actions/exec', () => { fs.writeFileSync(semaphorePath, '') const nodePath = await io.which('node', true) - const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js') const debugList: string[] = [] const _testExecOptions = getExecOptions() _testExecOptions.delay = 500 - _testExecOptions.windowsVerbatimArguments = true _testExecOptions.listeners = { debug: (data: string) => { debugList.push(data) } } - let exitCode: number - if (IS_WINDOWS) { - const toolName: string = await io.which('cmd.exe', true) - const args = [ - '/D', // Disable execution of AutoRun commands from registry. - '/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. - '/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. - '/S', // Will cause first and last quote after /C to be stripped. - '/C', - `"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}""` - ] - exitCode = await exec.exec(`"${toolName}"`, args, _testExecOptions) - } else { - const toolName: string = await io.which('bash', true) - const args = ['-c', `node '${scriptPath}' 'file=${semaphorePath}' &`] + const args = [SPAWN_WAIT_FOR_FILE, `file=${semaphorePath}`] - exitCode = await exec.exec(`"${toolName}"`, args, _testExecOptions) - } + const exitCode = await exec.exec(`"${nodePath}"`, args, _testExecOptions) expect(exitCode).toBe(0) expect( @@ -411,36 +399,19 @@ describe('@actions/exec', () => { fs.writeFileSync(semaphorePath, '') const nodePath = await io.which('node', true) - const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js') const debugList: string[] = [] const _testExecOptions = getExecOptions() _testExecOptions.delay = 500 - _testExecOptions.windowsVerbatimArguments = true _testExecOptions.listeners = { debug: (data: string) => { debugList.push(data) } } - let toolName: string - let args: string[] - if (IS_WINDOWS) { - toolName = await io.which('cmd.exe', true) - args = [ - '/D', // Disable execution of AutoRun commands from registry. - '/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. - '/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. - '/S', // Will cause first and last quote after /C to be stripped. - '/C', - `"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & exit /b 123` - ] - } else { - toolName = await io.which('bash', true) - args = ['-c', `node '${scriptPath}' 'file=${semaphorePath}' & exit 123`] - } + const args = [SPAWN_WAIT_FOR_FILE, `file=${semaphorePath}`, 'exitCode=123'] await exec - .exec(`"${toolName}"`, args, _testExecOptions) + .exec(`"${nodePath}"`, args, _testExecOptions) .then(() => { throw new Error('Should not have succeeded') }) @@ -465,40 +436,20 @@ describe('@actions/exec', () => { fs.writeFileSync(semaphorePath, '') const nodePath = await io.which('node', true) - const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js') const debugList: string[] = [] const _testExecOptions = getExecOptions() _testExecOptions.delay = 500 _testExecOptions.failOnStdErr = true - _testExecOptions.windowsVerbatimArguments = true _testExecOptions.listeners = { debug: (data: string) => { debugList.push(data) } } - let toolName: string - let args: string[] - if (IS_WINDOWS) { - toolName = await io.which('cmd.exe', true) - args = [ - '/D', // Disable execution of AutoRun commands from registry. - '/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry. - '/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry. - '/S', // Will cause first and last quote after /C to be stripped. - '/C', - `"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & echo hi 1>&2` - ] - } else { - toolName = await io.which('bash', true) - args = [ - '-c', - `node '${scriptPath}' 'file=${semaphorePath}' & echo hi 1>&2` - ] - } + const args = [SPAWN_WAIT_FOR_FILE, `file=${semaphorePath}`, 'stderr=true'] await exec - .exec(`"${toolName}"`, args, _testExecOptions) + .exec(`"${nodePath}"`, args, _testExecOptions) .then(() => { throw new Error('Should not have succeeded') }) diff --git a/__tests__/scripts/spawn-wait-for-file.js b/__tests__/scripts/spawn-wait-for-file.js new file mode 100644 index 0000000..31a6cd0 --- /dev/null +++ b/__tests__/scripts/spawn-wait-for-file.js @@ -0,0 +1,39 @@ +const childProcess = require('child_process') +const path = require('path') + +function parseArgs() { + const result = {} + for (const arg of process.argv.slice(2)) { + const equalsIndex = arg.indexOf('=') + if (equalsIndex === -1) { + continue + } + const key = arg.slice(0, equalsIndex) + const value = arg.slice(equalsIndex + 1) + result[key] = value + } + + return result +} + +const args = parseArgs() +const filePath = args.file +if (!filePath) { + throw new Error('file is not specified') +} + +const waitScript = path.join(__dirname, 'wait-for-file.js') +const waitArgs = [waitScript, `file=${filePath}`] +const waitProcess = childProcess.spawn(process.execPath, waitArgs, { + stdio: 'inherit', + detached: false +}) + +waitProcess.unref() + +if (args.stderr === 'true') { + process.stderr.write('hi') +} + +const exitCode = args.exitCode ? parseInt(args.exitCode, 10) : 0 +process.exit(exitCode) diff --git a/__tests__/scripts/stdoutputspecial.js b/__tests__/scripts/stdoutputspecial.js index 6a641cd..95f0ff4 100644 --- a/__tests__/scripts/stdoutputspecial.js +++ b/__tests__/scripts/stdoutputspecial.js @@ -1,8 +1,8 @@ //first half of © character -process.stdout.write(Buffer.from([0xC2]), (err) => { +process.stdout.write(Buffer.from([0xC2]), () => { //write in the callback so that the second byte is sent separately setTimeout(() => { process.stdout.write(Buffer.from([0xA9])) //second half of © character - }, 5000) + }, 100) }) From 04237d9dc5bdba95c244f47413dedaff7e7da08a Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Tue, 18 Nov 2025 15:45:59 +0000 Subject: [PATCH 2/5] tests(exec): use platform-aware spawn options in spawn-wait-for-file script (detach on Unix, hide window on Windows) --- __tests__/scripts/spawn-wait-for-file.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/__tests__/scripts/spawn-wait-for-file.js b/__tests__/scripts/spawn-wait-for-file.js index 31a6cd0..09e6a91 100644 --- a/__tests__/scripts/spawn-wait-for-file.js +++ b/__tests__/scripts/spawn-wait-for-file.js @@ -24,11 +24,23 @@ if (!filePath) { const waitScript = path.join(__dirname, 'wait-for-file.js') const waitArgs = [waitScript, `file=${filePath}`] -const waitProcess = childProcess.spawn(process.execPath, waitArgs, { - stdio: 'inherit', - detached: false -}) +// Spawn with inherited stdio and detached on Unix, non-detached on Windows +// This keeps the streams open after parent exits +const isWindows = process.platform === 'win32' +const spawnOptions = { + stdio: 'inherit', + detached: !isWindows +} + +// On Windows, we need to hide the window +if (isWindows) { + spawnOptions.windowsHide = true +} + +const waitProcess = childProcess.spawn(process.execPath, waitArgs, spawnOptions) + +// Unref so parent doesn't wait for child waitProcess.unref() if (args.stderr === 'true') { From 339756c0b9b5e5c6da56448acf0942180faf73ab Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Mon, 8 Dec 2025 12:27:43 +0000 Subject: [PATCH 3/5] fix: improve exec stream tests cross-platform handling - Update spawn-wait-for-file.js to use proper stdio inheritance - Add small delay before exit to ensure child process inherits handles - Simplify test code to use the helper script instead of shell commands --- __tests__/exec.test.ts | 28 +++++++----- __tests__/scripts/spawn-wait-for-file.js | 54 +++++++++--------------- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/__tests__/exec.test.ts b/__tests__/exec.test.ts index aff2ceb..130f382 100644 --- a/__tests__/exec.test.ts +++ b/__tests__/exec.test.ts @@ -11,7 +11,7 @@ import * as io from '@actions/io' /* eslint-disable @typescript-eslint/unbound-method */ const IS_WINDOWS = process.platform === 'win32' -const SPAWN_WAIT_FOR_FILE = path.join( +const SPAWN_WAIT_SCRIPT = path.join( __dirname, 'scripts', 'spawn-wait-for-file.js' @@ -379,9 +379,11 @@ describe('@actions/exec', () => { } } - const args = [SPAWN_WAIT_FOR_FILE, `file=${semaphorePath}`] - - const exitCode = await exec.exec(`"${nodePath}"`, args, _testExecOptions) + const exitCode = await exec.exec( + `"${nodePath}"`, + [SPAWN_WAIT_SCRIPT, `file=${semaphorePath}`], + _testExecOptions + ) expect(exitCode).toBe(0) expect( @@ -408,10 +410,12 @@ describe('@actions/exec', () => { } } - const args = [SPAWN_WAIT_FOR_FILE, `file=${semaphorePath}`, 'exitCode=123'] - await exec - .exec(`"${nodePath}"`, args, _testExecOptions) + .exec( + `"${nodePath}"`, + [SPAWN_WAIT_SCRIPT, `file=${semaphorePath}`, 'exitCode=123'], + _testExecOptions + ) .then(() => { throw new Error('Should not have succeeded') }) @@ -446,10 +450,12 @@ describe('@actions/exec', () => { } } - const args = [SPAWN_WAIT_FOR_FILE, `file=${semaphorePath}`, 'stderr=true'] - await exec - .exec(`"${nodePath}"`, args, _testExecOptions) + .exec( + `"${nodePath}"`, + [SPAWN_WAIT_SCRIPT, `file=${semaphorePath}`, 'stderr=true'], + _testExecOptions + ) .then(() => { throw new Error('Should not have succeeded') }) @@ -466,7 +472,7 @@ describe('@actions/exec', () => { ).toBe(1) fs.unlinkSync(semaphorePath) - }) + }, 10000) it('Exec roots relative tool path using unrooted options.cwd', async () => { let exitCode: number diff --git a/__tests__/scripts/spawn-wait-for-file.js b/__tests__/scripts/spawn-wait-for-file.js index 09e6a91..6c77749 100644 --- a/__tests__/scripts/spawn-wait-for-file.js +++ b/__tests__/scripts/spawn-wait-for-file.js @@ -1,51 +1,39 @@ const childProcess = require('child_process') const path = require('path') -function parseArgs() { - const result = {} - for (const arg of process.argv.slice(2)) { - const equalsIndex = arg.indexOf('=') - if (equalsIndex === -1) { - continue - } - const key = arg.slice(0, equalsIndex) - const value = arg.slice(equalsIndex + 1) - result[key] = value +// Parse args +const args = {} +for (const arg of process.argv.slice(2)) { + const idx = arg.indexOf('=') + if (idx !== -1) { + args[arg.slice(0, idx)] = arg.slice(idx + 1) } - - return result } -const args = parseArgs() const filePath = args.file if (!filePath) { throw new Error('file is not specified') } +// Spawn wait-for-file.js with inherited stdio +// This creates a grandchild process that holds the stdio handles open +// after this process (the child) exits const waitScript = path.join(__dirname, 'wait-for-file.js') -const waitArgs = [waitScript, `file=${filePath}`] +const child = childProcess.spawn(process.execPath, [waitScript, `file=${filePath}`], { + stdio: ['ignore', 'inherit', 'inherit'], + detached: process.platform !== 'win32' +}) -// Spawn with inherited stdio and detached on Unix, non-detached on Windows -// This keeps the streams open after parent exits -const isWindows = process.platform === 'win32' -const spawnOptions = { - stdio: 'inherit', - detached: !isWindows -} - -// On Windows, we need to hide the window -if (isWindows) { - spawnOptions.windowsHide = true -} - -const waitProcess = childProcess.spawn(process.execPath, waitArgs, spawnOptions) - -// Unref so parent doesn't wait for child -waitProcess.unref() +// Don't wait for child to exit +child.unref() +// Handle optional stderr output (must happen BEFORE we exit) if (args.stderr === 'true') { process.stderr.write('hi') } -const exitCode = args.exitCode ? parseInt(args.exitCode, 10) : 0 -process.exit(exitCode) +// Small delay to ensure child has started and inherited handles +setTimeout(() => { + const exitCode = args.exitCode ? parseInt(args.exitCode, 10) : 0 + process.exit(exitCode) +}, 50) From e356b02b96a47d21a1bae989dd9d8887e7ffc5d4 Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Mon, 8 Dec 2025 12:35:33 +0000 Subject: [PATCH 4/5] fix: use detached:true on all platforms for exec stream tests On Windows, detached:true is needed to properly keep stdio handles open after the parent process exits. --- __tests__/scripts/spawn-wait-for-file.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/__tests__/scripts/spawn-wait-for-file.js b/__tests__/scripts/spawn-wait-for-file.js index 6c77749..720b784 100644 --- a/__tests__/scripts/spawn-wait-for-file.js +++ b/__tests__/scripts/spawn-wait-for-file.js @@ -19,9 +19,14 @@ if (!filePath) { // This creates a grandchild process that holds the stdio handles open // after this process (the child) exits const waitScript = path.join(__dirname, 'wait-for-file.js') +const isWindows = process.platform === 'win32' + +// On Windows, use detached:true to properly keep streams open +// On Unix, detached:true also works and creates a new process group const child = childProcess.spawn(process.execPath, [waitScript, `file=${filePath}`], { stdio: ['ignore', 'inherit', 'inherit'], - detached: process.platform !== 'win32' + detached: true, + windowsHide: true }) // Don't wait for child to exit From 99cb09b2482b3f99219d31311652740dfad5f22f Mon Sep 17 00:00:00 2001 From: Salman Muin Kayser Chishti Date: Mon, 8 Dec 2025 12:47:19 +0000 Subject: [PATCH 5/5] Timeout time back to normal, as it was like this for debugging --- __tests__/scripts/stdoutputspecial.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/scripts/stdoutputspecial.js b/__tests__/scripts/stdoutputspecial.js index 95f0ff4..6a641cd 100644 --- a/__tests__/scripts/stdoutputspecial.js +++ b/__tests__/scripts/stdoutputspecial.js @@ -1,8 +1,8 @@ //first half of © character -process.stdout.write(Buffer.from([0xC2]), () => { +process.stdout.write(Buffer.from([0xC2]), (err) => { //write in the callback so that the second byte is sent separately setTimeout(() => { process.stdout.write(Buffer.from([0xA9])) //second half of © character - }, 100) + }, 5000) })