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