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
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ runs:
with:
node-version: "24"

- name: Validate action paths
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
CODEX_PROMPT_FILE: ${{ inputs['prompt-file'] }}
CODEX_WORKING_DIRECTORY: ${{ inputs['working-directory'] || github.workspace }}
run: node "$ACTION_PATH/scripts/validateActionPaths.mjs"

- name: Check repository write access
env:
ACTION_PATH: ${{ github.action_path }}
Expand Down
46 changes: 46 additions & 0 deletions scripts/validateActionPaths.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { access, stat } from "node:fs/promises";
import { constants } from "node:fs";

const promptFile = (process.env.CODEX_PROMPT_FILE ?? "").trim();
const workingDirectory = (process.env.CODEX_WORKING_DIRECTORY ?? "").trim();

if (promptFile) {
await requireReadableFile("prompt-file", promptFile);
}

if (!workingDirectory) {
throw new Error("working-directory resolved to an empty path");
}
await requireDirectory("working-directory", workingDirectory);

async function requireReadableFile(label, target) {
let info;
try {
info = await stat(target);
} catch {
throw new Error(`${label} does not exist: ${target}`);
}

if (info.isDirectory()) {
throw new Error(`${label} must reference a file, not a directory: ${target}`);
}

try {
await access(target, constants.R_OK);
} catch {
throw new Error(`${label} is not readable: ${target}`);
}
}

async function requireDirectory(label, target) {
let info;
try {
info = await stat(target);
} catch {
throw new Error(`${label} does not exist: ${target}`);
}

if (!info.isDirectory()) {
throw new Error(`${label} must reference a directory: ${target}`);
}
}
102 changes: 102 additions & 0 deletions test/validateActionPaths.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { test } from "node:test";
import { fileURLToPath } from "node:url";

const scriptPath = fileURLToPath(
new URL("../scripts/validateActionPaths.mjs", import.meta.url)
);

function run({ promptFile = "", workingDirectory }) {
return spawnSync(process.execPath, [scriptPath], {
encoding: "utf8",
env: {
...process.env,
CODEX_PROMPT_FILE: promptFile,
CODEX_WORKING_DIRECTORY: workingDirectory,
},
});
}

test("accepts a readable prompt file and existing working directory", () => {
const root = mkdtempSync(path.join(tmpdir(), "codex-path-test-"));
const promptFile = path.join(root, "prompt.md");
writeFileSync(promptFile, "Review this change", "utf8");

try {
const result = run({ promptFile, workingDirectory: root });
assert.equal(result.status, 0, result.stderr);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("allows an empty prompt-file when inline prompt is used", () => {
const root = mkdtempSync(path.join(tmpdir(), "codex-path-test-"));
try {
const result = run({ promptFile: " ", workingDirectory: root });
assert.equal(result.status, 0, result.stderr);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("rejects a missing prompt-file", () => {
const root = mkdtempSync(path.join(tmpdir(), "codex-path-test-"));
const promptFile = path.join(root, "missing.md");

try {
const result = run({ promptFile, workingDirectory: root });
assert.notEqual(result.status, 0);
assert.match(result.stderr, /prompt-file does not exist/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("rejects a directory used as prompt-file", () => {
const root = mkdtempSync(path.join(tmpdir(), "codex-path-test-"));
const promptDirectory = path.join(root, "prompt-dir");
mkdirSync(promptDirectory);

try {
const result = run({
promptFile: promptDirectory,
workingDirectory: root,
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /prompt-file must reference a file/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("rejects a missing working-directory", () => {
const root = mkdtempSync(path.join(tmpdir(), "codex-path-test-"));
const missingDirectory = path.join(root, "missing");

try {
const result = run({ workingDirectory: missingDirectory });
assert.notEqual(result.status, 0);
assert.match(result.stderr, /working-directory does not exist/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test("rejects a file used as working-directory", () => {
const root = mkdtempSync(path.join(tmpdir(), "codex-path-test-"));
const filePath = path.join(root, "not-a-directory");
writeFileSync(filePath, "x", "utf8");

try {
const result = run({ workingDirectory: filePath });
assert.notEqual(result.status, 0);
assert.match(result.stderr, /working-directory must reference a directory/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
Loading