處理 AI review findings 並改寫 Node.js entrypoint #2

Merged
admin merged 59 commits from develop into master 2026-06-24 14:13:11 +00:00
Showing only changes of commit f40de2a8fb - Show all commits
+53 -35
View File
@@ -10,6 +10,8 @@ const FILE_MODE_PRIVATE = 0o600;
const DIR_MODE_PRIVATE = 0o700; const DIR_MODE_PRIVATE = 0o700;
const DEFAULT_CODEX_TIMEOUT_MS = 30 * 60 * 1000; const DEFAULT_CODEX_TIMEOUT_MS = 30 * 60 * 1000;
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024; const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
const TEMP_DIR_PREFIX = ".codex-action-";
const OUTPUT_DELIMITER_PREFIX = "CODEX_OUTPUT_";
class TempFileRegistry { class TempFileRegistry {
constructor() { constructor() {
@@ -92,7 +94,7 @@ function cleanup() {
} }
function makeTempDir(dir) { function makeTempDir(dir) {
const tempDir = fs.mkdtempSync(path.join(dir, ".codex-action-")); const tempDir = fs.mkdtempSync(path.join(dir, TEMP_DIR_PREFIX));
fs.chmodSync(tempDir, DIR_MODE_PRIVATE); fs.chmodSync(tempDir, DIR_MODE_PRIVATE);
tempFiles.trackDir(tempDir); tempFiles.trackDir(tempDir);
return tempDir; return tempDir;
@@ -115,7 +117,7 @@ function appendGithubOutput(status, output) {
let delimiter; let delimiter;
do { do {
delimiter = `CODEX_OUTPUT_${crypto.randomBytes(12).toString("hex")}`; delimiter = `${OUTPUT_DELIMITER_PREFIX}${crypto.randomBytes(12).toString("hex")}`;
} while (output.includes(delimiter)); } while (output.includes(delimiter));
fs.appendFileSync( fs.appendFileSync(
@@ -132,22 +134,27 @@ function fail(message, code = 1) {
process.exit(code); process.exit(code);
} }
function normalizeBase64(value) { function compactBase64(value) {
return value.replace(/\s+/g, "").replace(/=+$/, ""); return value.replace(/\s+/g, "");
}
function isBase64(value) {
const normalized = compactBase64(value);
const paddingIndex = normalized.indexOf("=");
if (!normalized || normalized.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
return false;
}
return paddingIndex === -1 || /^=+$/.test(normalized.slice(paddingIndex));
} }
function validateAuth(encodedAuth, authFile) { function validateAuth(encodedAuth, authFile) {
const decoded = Buffer.from(encodedAuth, "base64"); if (!isBase64(encodedAuth)) {
const normalizedDecoded = normalizeBase64(decoded.toString("base64"));
const normalizedInput = normalizeBase64(encodedAuth);
if (decoded.length === 0 && normalizedInput.length > 0) {
fail("OAUTH must be valid base64 encoded Codex auth.json."); fail("OAUTH must be valid base64 encoded Codex auth.json.");
} }
if (normalizedDecoded !== normalizedInput) { const decoded = Buffer.from(compactBase64(encodedAuth), "base64");
fail("OAUTH must be valid base64 encoded Codex auth.json.");
}
fs.writeFileSync(authFile, decoded, { mode: FILE_MODE_PRIVATE }); fs.writeFileSync(authFile, decoded, { mode: FILE_MODE_PRIVATE });
@@ -168,26 +175,33 @@ function parsePositiveInteger(value, fallback) {
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
} }
function runCodex(model, prompt) { function readExecutionConfig() {
return new Promise((resolve) => { return {
const timeoutMs = parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS); timeoutMs: parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS),
const outputLimitBytes = parsePositiveInteger(process.env.CODEX_OUTPUT_LIMIT_BYTES, DEFAULT_OUTPUT_LIMIT_BYTES); outputLimitBytes: parsePositiveInteger(process.env.CODEX_OUTPUT_LIMIT_BYTES, DEFAULT_OUTPUT_LIMIT_BYTES),
const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); workspace: process.env.GITHUB_WORKSPACE || process.cwd(),
};
}
// This Docker Action runs inside an ephemeral CI container where Codex must be function codexExecArgs(model, prompt) {
// able to edit the checked-out workspace without interactive approvals. return [
const child = spawn(
"codex",
[
"exec", "exec",
"--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check", "--skip-git-repo-check",
"--model", "--model",
model, model,
prompt, prompt,
], ];
{ cwd: workspace, stdio: ["ignore", "pipe", "pipe"] }, }
);
function runCodex(model, prompt) {
return new Promise((resolve) => {
const { timeoutMs, outputLimitBytes, workspace } = readExecutionConfig();
const args = codexExecArgs(model, prompt);
// This Docker Action runs inside an ephemeral CI container where Codex must be
// able to edit the checked-out workspace without interactive approvals.
const child = spawn("codex", args, { cwd: workspace, stdio: ["ignore", "pipe", "pipe"] });
const output = new OutputCollector(outputLimitBytes); const output = new OutputCollector(outputLimitBytes);
@@ -255,6 +269,19 @@ function readConfig() {
return { oauth, model, codexHome, prompt }; return { oauth, model, codexHome, prompt };
} }
function createAuthLock(codexHome) {
const lockName = crypto.createHash("sha256").update(codexHome).digest("hex");
const lockPath = path.join(codexHome, `.codex-auth-${lockName}.lock`);
try {
const lockHandle = fs.openSync(lockPath, "wx", FILE_MODE_PRIVATE);
tempFiles.trackFile(lockPath);
return lockHandle;
} catch {
fail("Unable to lock Codex auth.json.");
}
}
function setupAuth(oauth, codexHome) { function setupAuth(oauth, codexHome) {
try { try {
fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE }); fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE });
@@ -267,16 +294,7 @@ function setupAuth(oauth, codexHome) {
const tempDir = makeTempDir(codexHome); const tempDir = makeTempDir(codexHome);
const authFile = makeTempFile(tempDir, "auth"); const authFile = makeTempFile(tempDir, "auth");
const authPath = path.join(codexHome, "auth.json"); const authPath = path.join(codexHome, "auth.json");
const lockName = crypto.createHash("sha256").update(codexHome).digest("hex"); const lockHandle = createAuthLock(codexHome);
const lockPath = path.join(codexHome, `.codex-auth-${lockName}.lock`);
let lockHandle;
try {
lockHandle = fs.openSync(lockPath, "wx", FILE_MODE_PRIVATE);
tempFiles.trackFile(lockPath);
} catch {
fail("Unable to lock Codex auth.json.");
}
validateAuth(oauth, authFile); validateAuth(oauth, authFile);