refactor(entrypoint): 收斂 Node.js 入口職責與輸出處理
This commit is contained in:
+38
-12
@@ -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));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user