處理 AI review findings 並改寫 Node.js entrypoint #2
+55
-37
@@ -10,6 +10,8 @@ const FILE_MODE_PRIVATE = 0o600;
|
|||||||
const DIR_MODE_PRIVATE = 0o700;
|
const DIR_MODE_PRIVATE = 0o700;
|
||||||
const DEFAULT_CODEX_TIMEOUT_MS = 30 * 60 * 1000;
|
const DEFAULT_CODEX_TIMEOUT_MS = 30 * 60 * 1000;
|
||||||
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
||||||
|
const TEMP_DIR_PREFIX = ".codex-action-";
|
||||||
|
const OUTPUT_DELIMITER_PREFIX = "CODEX_OUTPUT_";
|
||||||
|
|
||||||
class TempFileRegistry {
|
class TempFileRegistry {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -92,7 +94,7 @@ function cleanup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeTempDir(dir) {
|
function makeTempDir(dir) {
|
||||||
const tempDir = fs.mkdtempSync(path.join(dir, ".codex-action-"));
|
const tempDir = fs.mkdtempSync(path.join(dir, TEMP_DIR_PREFIX));
|
||||||
fs.chmodSync(tempDir, DIR_MODE_PRIVATE);
|
fs.chmodSync(tempDir, DIR_MODE_PRIVATE);
|
||||||
tempFiles.trackDir(tempDir);
|
tempFiles.trackDir(tempDir);
|
||||||
return tempDir;
|
return tempDir;
|
||||||
@@ -115,7 +117,7 @@ function appendGithubOutput(status, output) {
|
|||||||
|
|
||||||
let delimiter;
|
let delimiter;
|
||||||
do {
|
do {
|
||||||
delimiter = `CODEX_OUTPUT_${crypto.randomBytes(12).toString("hex")}`;
|
delimiter = `${OUTPUT_DELIMITER_PREFIX}${crypto.randomBytes(12).toString("hex")}`;
|
||||||
} while (output.includes(delimiter));
|
} while (output.includes(delimiter));
|
||||||
|
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
@@ -132,22 +134,27 @@ function fail(message, code = 1) {
|
|||||||
process.exit(code);
|
process.exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBase64(value) {
|
function compactBase64(value) {
|
||||||
return value.replace(/\s+/g, "").replace(/=+$/, "");
|
return value.replace(/\s+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBase64(value) {
|
||||||
|
const normalized = compactBase64(value);
|
||||||
|
const paddingIndex = normalized.indexOf("=");
|
||||||
|
|
||||||
|
if (!normalized || normalized.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return paddingIndex === -1 || /^=+$/.test(normalized.slice(paddingIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAuth(encodedAuth, authFile) {
|
function validateAuth(encodedAuth, authFile) {
|
||||||
const decoded = Buffer.from(encodedAuth, "base64");
|
if (!isBase64(encodedAuth)) {
|
||||||
const normalizedDecoded = normalizeBase64(decoded.toString("base64"));
|
|
||||||
const normalizedInput = normalizeBase64(encodedAuth);
|
|
||||||
|
|
||||||
if (decoded.length === 0 && normalizedInput.length > 0) {
|
|
||||||
fail("OAUTH must be valid base64 encoded Codex auth.json.");
|
fail("OAUTH must be valid base64 encoded Codex auth.json.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedDecoded !== normalizedInput) {
|
const decoded = Buffer.from(compactBase64(encodedAuth), "base64");
|
||||||
fail("OAUTH must be valid base64 encoded Codex auth.json.");
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(authFile, decoded, { mode: FILE_MODE_PRIVATE });
|
fs.writeFileSync(authFile, decoded, { mode: FILE_MODE_PRIVATE });
|
||||||
|
|
||||||
@@ -168,26 +175,33 @@ function parsePositiveInteger(value, fallback) {
|
|||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readExecutionConfig() {
|
||||||
|
return {
|
||||||
|
timeoutMs: parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS),
|
||||||
|
outputLimitBytes: parsePositiveInteger(process.env.CODEX_OUTPUT_LIMIT_BYTES, DEFAULT_OUTPUT_LIMIT_BYTES),
|
||||||
|
workspace: process.env.GITHUB_WORKSPACE || process.cwd(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function codexExecArgs(model, prompt) {
|
||||||
|
return [
|
||||||
|
"exec",
|
||||||
|
"--dangerously-bypass-approvals-and-sandbox",
|
||||||
|
"--skip-git-repo-check",
|
||||||
|
"--model",
|
||||||
|
model,
|
||||||
|
prompt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function runCodex(model, prompt) {
|
function runCodex(model, prompt) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const timeoutMs = parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS);
|
const { timeoutMs, outputLimitBytes, workspace } = readExecutionConfig();
|
||||||
const outputLimitBytes = parsePositiveInteger(process.env.CODEX_OUTPUT_LIMIT_BYTES, DEFAULT_OUTPUT_LIMIT_BYTES);
|
const args = codexExecArgs(model, prompt);
|
||||||
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
|
||||||
|
|
||||||
// This Docker Action runs inside an ephemeral CI container where Codex must be
|
// This Docker Action runs inside an ephemeral CI container where Codex must be
|
||||||
// able to edit the checked-out workspace without interactive approvals.
|
// able to edit the checked-out workspace without interactive approvals.
|
||||||
const child = spawn(
|
const child = spawn("codex", args, { cwd: workspace, stdio: ["ignore", "pipe", "pipe"] });
|
||||||
"codex",
|
|
||||||
[
|
|
||||||
"exec",
|
|
||||||
"--dangerously-bypass-approvals-and-sandbox",
|
|
||||||
"--skip-git-repo-check",
|
|
||||||
"--model",
|
|
||||||
model,
|
|
||||||
prompt,
|
|
||||||
],
|
|
||||||
{ cwd: workspace, stdio: ["ignore", "pipe", "pipe"] },
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = new OutputCollector(outputLimitBytes);
|
const output = new OutputCollector(outputLimitBytes);
|
||||||
|
|
||||||
@@ -255,6 +269,19 @@ function readConfig() {
|
|||||||
return { oauth, model, codexHome, prompt };
|
return { oauth, model, codexHome, prompt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createAuthLock(codexHome) {
|
||||||
|
const lockName = crypto.createHash("sha256").update(codexHome).digest("hex");
|
||||||
|
const lockPath = path.join(codexHome, `.codex-auth-${lockName}.lock`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lockHandle = fs.openSync(lockPath, "wx", FILE_MODE_PRIVATE);
|
||||||
|
tempFiles.trackFile(lockPath);
|
||||||
|
return lockHandle;
|
||||||
|
} catch {
|
||||||
|
fail("Unable to lock Codex auth.json.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupAuth(oauth, codexHome) {
|
function setupAuth(oauth, codexHome) {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE });
|
fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE });
|
||||||
@@ -267,16 +294,7 @@ function setupAuth(oauth, codexHome) {
|
|||||||
const tempDir = makeTempDir(codexHome);
|
const tempDir = makeTempDir(codexHome);
|
||||||
const authFile = makeTempFile(tempDir, "auth");
|
const authFile = makeTempFile(tempDir, "auth");
|
||||||
const authPath = path.join(codexHome, "auth.json");
|
const authPath = path.join(codexHome, "auth.json");
|
||||||
const lockName = crypto.createHash("sha256").update(codexHome).digest("hex");
|
const lockHandle = createAuthLock(codexHome);
|
||||||
const lockPath = path.join(codexHome, `.codex-auth-${lockName}.lock`);
|
|
||||||
|
|
||||||
let lockHandle;
|
|
||||||
try {
|
|
||||||
lockHandle = fs.openSync(lockPath, "wx", FILE_MODE_PRIVATE);
|
|
||||||
tempFiles.trackFile(lockPath);
|
|
||||||
} catch {
|
|
||||||
fail("Unable to lock Codex auth.json.");
|
|
||||||
}
|
|
||||||
|
|
||||||
validateAuth(oauth, authFile);
|
validateAuth(oauth, authFile);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user