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