#!/usr/bin/env node const fs = require("fs"); const os = require("os"); const path = require("path"); const crypto = require("crypto"); const { spawn } = require("child_process"); const DEFAULT_PROMPT = "請自我介紹"; const createdPaths = new Set(); function removeIfCreated(filePath) { if (!filePath || !createdPaths.has(filePath)) { return; } try { fs.rmSync(filePath, { force: true }); } catch (error) { console.error(`Unable to remove temporary file: ${error.message}`); } } function cleanup() { for (const filePath of Array.from(createdPaths).reverse()) { removeIfCreated(filePath); } } function makeTempFile(dir, prefix) { const random = crypto.randomBytes(16).toString("hex"); const filePath = path.join(dir, `${prefix}.${random}`); const fd = fs.openSync(filePath, "wx", 0o600); fs.closeSync(fd); createdPaths.add(filePath); return filePath; } function appendGithubOutput(status, output) { const outputFile = process.env.GITHUB_OUTPUT; if (!outputFile) { return; } let delimiter; do { delimiter = `CODEX_OUTPUT_${crypto.randomBytes(12).toString("hex")}`; } while (output.includes(delimiter)); fs.appendFileSync( outputFile, `status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`, { encoding: "utf8", mode: 0o600 }, ); } function fail(message, code = 1) { console.error(message); appendGithubOutput("failed", message); cleanup(); process.exit(code); } 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 && normalizedAuth.length > 0) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } if (normalizedDecoded !== normalizedInput) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } fs.writeFileSync(authFile, decoded, { mode: 0o600 }); let parsed; try { parsed = JSON.parse(decoded.toString("utf8")); } catch { fail("Decoded OAUTH must be a JSON object."); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") { fail("Decoded OAUTH must be a JSON object."); } } 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", [ "exec", "--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check", "--model", model, prompt, ], { stdio: ["ignore", "pipe", "pipe"] }, ); const outputChunks = []; const appendOutput = (chunk) => { outputChunks.push(chunk); return Buffer.concat(outputChunks).toString(); }; child.stdout.on("data", (chunk) => { process.stdout.write(chunk); outputChunks.push(chunk); }); child.stderr.on("data", (chunk) => { process.stdout.write(chunk); outputChunks.push(chunk); }); child.on("error", (error) => { 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 }); }); }); } function registerCleanupHandlers() { process.on("exit", cleanup); process.on("SIGINT", () => { cleanup(); process.exit(130); }); process.on("SIGTERM", () => { cleanup(); process.exit(143); }); } function readConfig() { const oauth = process.env.OAUTH || ""; const model = process.env.MODEL || ""; const codexHome = process.env.CODEX_HOME || "/root/.codex"; const prompt = process.env.PROMPT || DEFAULT_PROMPT; if (!oauth) { fail("OAUTH is required: provide base64 encoded Codex auth.json."); } if (!model) { fail("MODEL is required."); } return { oauth, model, codexHome, prompt }; } function setupAuth(oauth, codexHome) { try { fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 }); } catch { fail("Unable to create CODEX_HOME."); } const authFile = makeTempFile(codexHome, "auth"); const authPath = path.join(codexHome, "auth.json"); const lockPath = path.join(os.tmpdir(), `codex-auth-${Buffer.from(codexHome).toString("hex")}.lock`); let lockHandle; try { lockHandle = fs.openSync(lockPath, "wx", 0o600); createdPaths.add(lockPath); } catch { fail("Unable to lock Codex auth.json."); } validateAuth(oauth, authFile); if (fs.existsSync(authPath)) { fail("Refusing to overwrite existing Codex auth.json."); } fs.copyFileSync(authFile, authPath); fs.chmodSync(authPath, 0o600); 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); if (lockHandle !== undefined) { fs.closeSync(lockHandle); } cleanup(); process.exit(result.status); } async function main() { registerCleanupHandlers(); await runCodexAction(readConfig()); } main().catch((error) => { fail(error instanceof Error ? error.message : String(error)); });