From 83de97b35ad22b8aec035315976346266f6ef9d0 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Wed, 24 Jun 2026 13:54:11 +0000 Subject: [PATCH] =?UTF-8?q?refactor(entrypoint):=20=E5=BC=B7=E5=8C=96?= =?UTF-8?q?=E6=9A=AB=E5=AD=98=E6=AA=94=E8=88=87=20Codex=20=E5=9F=B7?= =?UTF-8?q?=E8=A1=8C=E6=8E=A7=E7=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.js | 142 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 110 insertions(+), 32 deletions(-) diff --git a/app/main.js b/app/main.js index 7933bed..2f3323a 100644 --- a/app/main.js +++ b/app/main.js @@ -1,38 +1,78 @@ #!/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(); +const FILE_MODE_PRIVATE = 0o600; +const DIR_MODE_PRIVATE = 0o700; +const DEFAULT_CODEX_TIMEOUT_MS = 30 * 60 * 1000; +const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024; -function removeIfCreated(filePath) { - if (!filePath || !createdPaths.has(filePath)) { - return; +class TempFileRegistry { + constructor() { + this.files = new Set(); + this.dirs = new Set(); } - try { - fs.rmSync(filePath, { force: true }); - } catch (error) { - console.error(`Unable to remove temporary file: ${error.message}`); + trackFile(filePath) { + this.files.add(filePath); + } + + trackDir(dirPath) { + this.dirs.add(dirPath); + } + + removeFile(filePath) { + if (!filePath || !this.files.has(filePath)) { + return; + } + + try { + fs.rmSync(filePath, { force: true }); + this.files.delete(filePath); + } catch (error) { + console.error(`Unable to remove temporary file: ${error.message}`); + } + } + + cleanup() { + for (const filePath of Array.from(this.files).reverse()) { + this.removeFile(filePath); + } + + for (const dirPath of Array.from(this.dirs).reverse()) { + try { + fs.rmSync(dirPath, { force: true, recursive: true }); + this.dirs.delete(dirPath); + } catch (error) { + console.error(`Unable to remove temporary directory: ${error.message}`); + } + } } } +const tempFiles = new TempFileRegistry(); + function cleanup() { - for (const filePath of Array.from(createdPaths).reverse()) { - removeIfCreated(filePath); - } + tempFiles.cleanup(); +} + +function makeTempDir(dir) { + const tempDir = fs.mkdtempSync(path.join(dir, ".codex-action-")); + fs.chmodSync(tempDir, DIR_MODE_PRIVATE); + tempFiles.trackDir(tempDir); + return tempDir; } 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); + const fd = fs.openSync(filePath, "wx", FILE_MODE_PRIVATE); fs.closeSync(fd); - createdPaths.add(filePath); + tempFiles.trackFile(filePath); return filePath; } @@ -50,7 +90,7 @@ function appendGithubOutput(status, output) { fs.appendFileSync( outputFile, `status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`, - { encoding: "utf8", mode: 0o600 }, + { encoding: "utf8", mode: FILE_MODE_PRIVATE }, ); } @@ -75,7 +115,7 @@ function validateAuth(encodedAuth, authFile) { fail("OAUTH must be valid base64 encoded Codex auth.json."); } - fs.writeFileSync(authFile, decoded, { mode: 0o600 }); + fs.writeFileSync(authFile, decoded, { mode: FILE_MODE_PRIVATE }); let parsed; try { @@ -89,8 +129,29 @@ function validateAuth(encodedAuth, authFile) { } } +function parsePositiveInteger(value, fallback) { + const parsed = Number.parseInt(value || "", 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function truncateOutput(chunks, nextChunk, maxBytes) { + const currentSize = chunks.reduce((total, chunk) => total + chunk.length, 0); + const available = maxBytes - currentSize; + + if (available <= 0) { + return false; + } + + chunks.push(nextChunk.length > available ? nextChunk.subarray(0, available) : nextChunk); + return nextChunk.length <= available; +} + function runCodex(model, prompt) { return new Promise((resolve) => { + const timeoutMs = parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS); + const outputLimitBytes = parsePositiveInteger(process.env.CODEX_OUTPUT_LIMIT_BYTES, DEFAULT_OUTPUT_LIMIT_BYTES); + const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); + // 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( @@ -103,32 +164,48 @@ function runCodex(model, prompt) { model, prompt, ], - { stdio: ["ignore", "pipe", "pipe"] }, + { cwd: workspace, stdio: ["ignore", "pipe", "pipe"] }, ); const outputChunks = []; + let outputTruncated = false; const appendOutput = (chunk) => { - outputChunks.push(chunk); + if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) { + outputTruncated = true; + } return Buffer.concat(outputChunks).toString(); }; + const timeout = setTimeout(() => { + child.kill("SIGTERM"); + appendOutput(Buffer.from(`Codex execution timed out after ${timeoutMs} ms.\n`)); + }, timeoutMs); + + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stdout); + child.stdout.on("data", (chunk) => { - process.stdout.write(chunk); - outputChunks.push(chunk); + if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) { + outputTruncated = true; + } }); child.stderr.on("data", (chunk) => { - process.stdout.write(chunk); - outputChunks.push(chunk); + if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) { + outputTruncated = true; + } }); child.on("error", (error) => { + clearTimeout(timeout); const output = appendOutput(Buffer.from(`${error.message}\n`)); resolve({ status: 1, output }); }); child.on("close", (code) => { - const output = Buffer.concat(outputChunks).toString(); + clearTimeout(timeout); + const truncationMessage = outputTruncated ? "\n[Output truncated]\n" : ""; + const output = `${Buffer.concat(outputChunks).toString()}${truncationMessage}`; resolve({ status: code ?? 1, output }); }); }); @@ -165,19 +242,21 @@ function readConfig() { function setupAuth(oauth, codexHome) { try { - fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 }); + fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE }); } catch { fail("Unable to create CODEX_HOME."); } - const authFile = makeTempFile(codexHome, "auth"); + const tempDir = makeTempDir(codexHome); + const authFile = makeTempFile(tempDir, "auth"); const authPath = path.join(codexHome, "auth.json"); - const lockPath = path.join(os.tmpdir(), `codex-auth-${Buffer.from(codexHome).toString("hex")}.lock`); + const lockName = crypto.createHash("sha256").update(codexHome).digest("hex"); + const lockPath = path.join(codexHome, `.codex-auth-${lockName}.lock`); let lockHandle; try { - lockHandle = fs.openSync(lockPath, "wx", 0o600); - createdPaths.add(lockPath); + lockHandle = fs.openSync(lockPath, "wx", FILE_MODE_PRIVATE); + tempFiles.trackFile(lockPath); } catch { fail("Unable to lock Codex auth.json."); } @@ -188,10 +267,9 @@ function setupAuth(oauth, codexHome) { fail("Refusing to overwrite existing Codex auth.json."); } - fs.copyFileSync(authFile, authPath); - fs.chmodSync(authPath, 0o600); - createdPaths.add(authPath); - removeIfCreated(authFile); + fs.renameSync(authFile, authPath); + fs.chmodSync(authPath, FILE_MODE_PRIVATE); + tempFiles.trackFile(authPath); return lockHandle; }