diff --git a/app/main.js b/app/main.js index bfa0c75..7933bed 100644 --- a/app/main.js +++ b/app/main.js @@ -3,6 +3,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); +const crypto = require("crypto"); const { spawn } = require("child_process"); const DEFAULT_PROMPT = "請自我介紹"; @@ -15,8 +16,8 @@ function removeIfCreated(filePath) { try { fs.rmSync(filePath, { force: true }); - } catch { - // Best-effort cleanup only. + } catch (error) { + console.error(`Unable to remove temporary file: ${error.message}`); } } @@ -27,7 +28,7 @@ function cleanup() { } function makeTempFile(dir, prefix) { - const random = `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`; + const random = crypto.randomBytes(16).toString("hex"); const filePath = path.join(dir, `${prefix}.${random}`); const fd = fs.openSync(filePath, "wx", 0o600); fs.closeSync(fd); @@ -43,7 +44,7 @@ function appendGithubOutput(status, output) { let delimiter; do { - delimiter = `CODEX_OUTPUT_${Math.random().toString(36).slice(2)}${Date.now()}`; + delimiter = `CODEX_OUTPUT_${crypto.randomBytes(12).toString("hex")}`; } while (output.includes(delimiter)); fs.appendFileSync( @@ -61,14 +62,16 @@ function fail(message, code = 1) { } function validateAuth(encodedAuth, authFile) { + const normalizedAuth = encodedAuth.replace(/\s+/g, ""); const decoded = Buffer.from(encodedAuth, "base64"); + const normalizedDecoded = decoded.toString("base64").replace(/=+$/, ""); + const normalizedInput = normalizedAuth.replace(/=+$/, ""); - if (decoded.length === 0 && encodedAuth.length > 0) { + if (decoded.length === 0 && normalizedAuth.length > 0) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } - const normalized = encodedAuth.replace(/\s+/g, ""); - if (decoded.toString("base64").replace(/=+$/, "") !== normalized.replace(/=+$/, "")) { + if (normalizedDecoded !== normalizedInput) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } @@ -88,6 +91,8 @@ function validateAuth(encodedAuth, authFile) { function runCodex(model, prompt) { return new Promise((resolve) => { + // 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", [ @@ -101,30 +106,35 @@ function runCodex(model, prompt) { { stdio: ["ignore", "pipe", "pipe"] }, ); - let output = ""; + const outputChunks = []; + const appendOutput = (chunk) => { + outputChunks.push(chunk); + return Buffer.concat(outputChunks).toString(); + }; child.stdout.on("data", (chunk) => { process.stdout.write(chunk); - output += chunk.toString(); + outputChunks.push(chunk); }); child.stderr.on("data", (chunk) => { process.stdout.write(chunk); - output += chunk.toString(); + outputChunks.push(chunk); }); child.on("error", (error) => { - output += `${error.message}\n`; + const output = appendOutput(Buffer.from(`${error.message}\n`)); resolve({ status: 1, output }); }); child.on("close", (code) => { + const output = Buffer.concat(outputChunks).toString(); resolve({ status: code ?? 1, output }); }); }); } -async function main() { +function registerCleanupHandlers() { process.on("exit", cleanup); process.on("SIGINT", () => { cleanup(); @@ -134,7 +144,9 @@ async function main() { cleanup(); process.exit(143); }); +} +function readConfig() { const oauth = process.env.OAUTH || ""; const model = process.env.MODEL || ""; const codexHome = process.env.CODEX_HOME || "/root/.codex"; @@ -148,6 +160,10 @@ async function main() { fail("MODEL is required."); } + return { oauth, model, codexHome, prompt }; +} + +function setupAuth(oauth, codexHome) { try { fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 }); } catch { @@ -177,6 +193,11 @@ async function main() { createdPaths.add(authPath); removeIfCreated(authFile); + return lockHandle; +} + +async function runCodexAction({ oauth, model, codexHome, prompt }) { + const lockHandle = setupAuth(oauth, codexHome); const result = await runCodex(model, prompt); appendGithubOutput(result.status === 0 ? "completed" : "failed", result.output); @@ -188,6 +209,11 @@ async function main() { process.exit(result.status); } +async function main() { + registerCleanupHandlers(); + await runCodexAction(readConfig()); +} + main().catch((error) => { fail(error instanceof Error ? error.message : String(error)); });