From 79227718f875e1736b717a4c6452559a8d3ba99d Mon Sep 17 00:00:00 2001 From: Jean Schmidt Date: Fri, 12 Jun 2026 07:25:34 -0700 Subject: [PATCH] Create parent dir before writing entry-point scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add fs.mkdirSync(path.dirname(...), { recursive: true }) before each of the three writeFileSync calls in prepareJobScript, writeRunScript, and writeContainerStepScript. - Add explicit RUNNER_TEMP guard at the top of prepareJobScript and writeRunScript so a missing env var still fails fast instead of creating a literal "undefined/" directory. - Add tests covering all three writers with a non-existent parent dir. Notes: fs.writeFileSync throws ENOENT when the target file's parent directory does not exist. In normal job flows the parent is created early, but cleanup races, custom RUNNER_TEMP overrides, and fresh container-step dst directories can leave it missing — surfacing as a confusing, indirect failure. The mkdirSync call is cheap, idempotent, and removes the failure mode entirely. The explicit RUNNER_TEMP guard preserves the prior contract: before this change, "RUNNER_TEMP unset" failed implicitly via ENOENT on "undefined/.sh". With mkdirSync added that path would silently create a literal "undefined/" directory in cwd, so the guard is required to keep the failure visible. --- packages/k8s/src/k8s/utils.ts | 16 +++++++-- packages/k8s/tests/k8s-utils-test.ts | 53 ++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/k8s/src/k8s/utils.ts b/packages/k8s/src/k8s/utils.ts index 20da57a3..51ed4245 100644 --- a/packages/k8s/src/k8s/utils.ts +++ b/packages/k8s/src/k8s/utils.ts @@ -1,5 +1,6 @@ import * as k8s from '@kubernetes/client-node' import * as fs from 'fs' +import * as path from 'path' import * as yaml from 'js-yaml' import * as core from '@actions/core' import { v1 as uuidv4 } from 'uuid' @@ -45,8 +46,13 @@ cp -R /__w/_temp/_github_workflow /github/workflow mkdir -p ${mountDirs} ` + const runnerTemp = process.env.RUNNER_TEMP + if (!runnerTemp) { + throw new Error('RUNNER_TEMP is not set') + } const filename = `${uuidv4()}.sh` - const entryPointPath = `${process.env.RUNNER_TEMP}/${filename}` + const entryPointPath = `${runnerTemp}/${filename}` + fs.mkdirSync(path.dirname(entryPointPath), { recursive: true }) fs.writeFileSync(entryPointPath, content) return { containerPath: `/__w/_temp/${filename}`, @@ -80,8 +86,13 @@ exec ${environmentPrefix} ${entryPoint} ${ entryPointArgs?.length ? entryPointArgs.join(' ') : '' } ` + const runnerTemp = process.env.RUNNER_TEMP + if (!runnerTemp) { + throw new Error('RUNNER_TEMP is not set') + } const filename = `${uuidv4()}.sh` - const entryPointPath = `${process.env.RUNNER_TEMP}/${filename}` + const entryPointPath = `${runnerTemp}/${filename}` + fs.mkdirSync(path.dirname(entryPointPath), { recursive: true }) fs.writeFileSync(entryPointPath, content) return { containerPath: `/__w/_temp/${filename}`, @@ -117,6 +128,7 @@ exec ${environmentPrefix} ${entryPoint} ${ const filename = `${uuidv4()}.sh` const entryPointPath = `${dst}/${filename}` core.debug(`Writing container step script to ${entryPointPath}`) + fs.mkdirSync(path.dirname(entryPointPath), { recursive: true }) fs.writeFileSync(entryPointPath, content) return { containerPath: `/__w/_temp/${filename}`, diff --git a/packages/k8s/tests/k8s-utils-test.ts b/packages/k8s/tests/k8s-utils-test.ts index bfb6c453..bc836286 100644 --- a/packages/k8s/tests/k8s-utils-test.ts +++ b/packages/k8s/tests/k8s-utils-test.ts @@ -1,7 +1,11 @@ import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' import { containerPorts } from '../src/k8s' import { generateContainerName, + prepareJobScript, + writeContainerStepScript, writeRunScript, mergePodSpecWithOptions, mergeContainerWithOptions, @@ -121,6 +125,55 @@ describe('k8s utils', () => { }) }) + describe('defensive parent-dir creation', () => { + let tempRoot: string + + beforeEach(() => { + tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pr05-')) + }) + + afterEach(() => { + fs.rmSync(tempRoot, { recursive: true, force: true }) + }) + + it('writeRunScript creates RUNNER_TEMP parent directory when missing', () => { + const missingDir = path.join(tempRoot, 'nested', 'runner_temp') + expect(fs.existsSync(missingDir)).toBe(false) + process.env.RUNNER_TEMP = missingDir + + const { runnerPath } = writeRunScript('/test', 'sh', ['-e', 'script.sh']) + + expect(fs.existsSync(missingDir)).toBe(true) + expect(fs.existsSync(runnerPath)).toBe(true) + }) + + it('prepareJobScript creates RUNNER_TEMP parent directory when missing', () => { + const missingDir = path.join(tempRoot, 'nested', 'runner_temp') + expect(fs.existsSync(missingDir)).toBe(false) + process.env.RUNNER_TEMP = missingDir + + const { runnerPath } = prepareJobScript([]) + + expect(fs.existsSync(missingDir)).toBe(true) + expect(fs.existsSync(runnerPath)).toBe(true) + }) + + it('writeContainerStepScript creates dst parent directory when missing', () => { + const missingDst = path.join(tempRoot, 'nested', 'step_dst') + expect(fs.existsSync(missingDst)).toBe(false) + + const { runnerPath } = writeContainerStepScript( + missingDst, + '/__w/repo/repo', + 'sh', + ['-e', 'script.sh'] + ) + + expect(fs.existsSync(missingDst)).toBe(true) + expect(fs.existsSync(runnerPath)).toBe(true) + }) + }) + describe('container volumes', () => { beforeEach(async () => { testHelper = new TestHelper()