處理 AI review findings 並改寫 Node.js entrypoint #2

Merged
admin merged 59 commits from develop into master 2026-06-24 14:13:11 +00:00
3 changed files with 199 additions and 83 deletions
Showing only changes of commit eac0a4463a - Show all commits
+3 -2
View File
@@ -9,7 +9,7 @@ ARG CODEX_DOC_MARKETPLACE_REF=f8da961328a85f267b4402566e127310370169da
ARG CODEX_CODE_REVIEW_MARKETPLACE_REF=9e016edff016d58f4d64e0a5468220d35a0f657b
# 安裝必要的工具
RUN apk add --no-cache --no-check-certificate bash ca-certificates curl git jq util-linux
RUN apk add --no-cache --no-check-certificate bash ca-certificates curl git jq nodejs
# 安裝 Codex CLI 工具
RUN install_script="$(mktemp)" \
@@ -27,8 +27,9 @@ RUN codex plugin marketplace add "https://gitea.jsc.idv.tw/plugins/doc.git" --re
&& codex plugin add "jsc@doc" \
&& codex plugin add "jsc@code-review"
COPY app /app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN chmod +x /entrypoint.sh /app/main.js
ENTRYPOINT ["/entrypoint.sh"]
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env node
const fs = require("fs");
const os = require("os");
const path = require("path");
const { spawn } = require("child_process");
const DEFAULT_PROMPT = "請自我介紹";
const createdPaths = new Set();
function removeIfCreated(filePath) {
if (!filePath || !createdPaths.has(filePath)) {
return;
}
try {
fs.rmSync(filePath, { force: true });
} catch {
// Best-effort cleanup only.
}
}
function cleanup() {
for (const filePath of Array.from(createdPaths).reverse()) {
removeIfCreated(filePath);
}
}
function makeTempFile(dir, prefix) {
const random = `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`;
const filePath = path.join(dir, `${prefix}.${random}`);
const fd = fs.openSync(filePath, "wx", 0o600);
fs.closeSync(fd);
createdPaths.add(filePath);
return filePath;
}
function appendGithubOutput(status, output) {
const outputFile = process.env.GITHUB_OUTPUT;
if (!outputFile) {
return;
}
let delimiter;
do {
delimiter = `CODEX_OUTPUT_${Math.random().toString(36).slice(2)}${Date.now()}`;
} while (output.includes(delimiter));
fs.appendFileSync(
outputFile,
`status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`,
{ encoding: "utf8", mode: 0o600 },
);
}
function fail(message, code = 1) {
console.error(message);
appendGithubOutput("failed", message);
cleanup();
process.exit(code);
}
function validateAuth(encodedAuth, authFile) {
const decoded = Buffer.from(encodedAuth, "base64");
if (decoded.length === 0 && encodedAuth.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(/=+$/, "")) {
fail("OAUTH must be valid base64 encoded Codex auth.json.");
}
fs.writeFileSync(authFile, decoded, { mode: 0o600 });
let parsed;
try {
parsed = JSON.parse(decoded.toString("utf8"));
} catch {
fail("Decoded OAUTH must be a JSON object.");
}
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
fail("Decoded OAUTH must be a JSON object.");
}
}
function runCodex(model, prompt) {
return new Promise((resolve) => {
const child = spawn(
"codex",
[
"exec",
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
"--model",
model,
prompt,
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let output = "";
child.stdout.on("data", (chunk) => {
process.stdout.write(chunk);
output += chunk.toString();
});
child.stderr.on("data", (chunk) => {
process.stdout.write(chunk);
output += chunk.toString();
});
child.on("error", (error) => {
output += `${error.message}\n`;
resolve({ status: 1, output });
});
child.on("close", (code) => {
resolve({ status: code ?? 1, output });
});
});
}
async function main() {
process.on("exit", cleanup);
process.on("SIGINT", () => {
cleanup();
process.exit(130);
});
process.on("SIGTERM", () => {
cleanup();
process.exit(143);
});
const oauth = process.env.OAUTH || "";
const model = process.env.MODEL || "";
const codexHome = process.env.CODEX_HOME || "/root/.codex";
const prompt = process.env.PROMPT || DEFAULT_PROMPT;
if (!oauth) {
fail("OAUTH is required: provide base64 encoded Codex auth.json.");
}
if (!model) {
fail("MODEL is required.");
}
try {
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
} catch {
fail("Unable to create CODEX_HOME.");
}
const authFile = makeTempFile(codexHome, "auth");
const authPath = path.join(codexHome, "auth.json");
const lockPath = path.join(os.tmpdir(), `codex-auth-${Buffer.from(codexHome).toString("hex")}.lock`);
let lockHandle;
try {
lockHandle = fs.openSync(lockPath, "wx", 0o600);
createdPaths.add(lockPath);
} catch {
fail("Unable to lock Codex auth.json.");
}
validateAuth(oauth, authFile);
if (fs.existsSync(authPath)) {
fail("Refusing to overwrite existing Codex auth.json.");
}
fs.copyFileSync(authFile, authPath);
fs.chmodSync(authPath, 0o600);
createdPaths.add(authPath);
removeIfCreated(authFile);
const result = await runCodex(model, prompt);
appendGithubOutput(result.status === 0 ? "completed" : "failed", result.output);
if (lockHandle !== undefined) {
fs.closeSync(lockHandle);
}
cleanup();
process.exit(result.status);
}
main().catch((error) => {
fail(error instanceof Error ? error.message : String(error));
});
+3 -81
View File
@@ -1,85 +1,7 @@
#!/bin/bash
set -eo pipefail
set -euo pipefail
die() {
echo "$1" >&2
exit 1
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cleanup() {
rm -f "${auth_file:-}" "${auth_path:-}" "${auth_lock:-}" "${codex_output:-}"
}
trap cleanup EXIT
if [[ -z "${OAUTH:-}" ]]; then
die "OAUTH is required: provide base64 encoded Codex auth.json."
fi
if [[ -z "${MODEL:-}" ]]; then
die "MODEL is required."
fi
CODEX_HOME="${CODEX_HOME:-/root/.codex}"
PROMPT="${PROMPT:-請自我介紹}"
mkdir -p "$CODEX_HOME" || die "Unable to create CODEX_HOME."
umask 077
auth_file="$(mktemp "$CODEX_HOME/auth.XXXXXX")"
auth_path="$CODEX_HOME/auth.json"
auth_lock="$(mktemp "$CODEX_HOME/auth.lock.XXXXXX")"
exec 9>"$auth_lock"
flock -n 9 || die "Unable to lock Codex auth.json."
if ! printf '%s\n' "$OAUTH" | base64 -d > "$auth_file"; then
die "OAUTH must be valid base64 encoded Codex auth.json."
fi
if ! jq -e 'type == "object"' "$auth_file" >/dev/null; then
die "Decoded OAUTH must be a JSON object."
fi
if [[ -e "$auth_path" ]]; then
die "Refusing to overwrite existing Codex auth.json."
fi
install -m 600 "$auth_file" "$auth_path"
rm -f "$auth_file"
codex_output="$(mktemp)"
if codex exec \
--dangerously-bypass-approvals-and-sandbox \
--skip-git-repo-check \
--model "$MODEL" \
"$PROMPT" 2>&1 | tee "$codex_output"; then
codex_status=0
else
codex_status="${PIPESTATUS[0]}"
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
while :; do
output_delimiter="CODEX_OUTPUT_$(mktemp -u XXXXXXXXXXXXXXXX)"
if ! grep -qxF "$output_delimiter" "$codex_output"; then
break
fi
done
if [[ "$codex_status" -eq 0 ]]; then
echo "status=completed" >> "$GITHUB_OUTPUT"
else
echo "status=failed" >> "$GITHUB_OUTPUT"
fi
{
echo "output<<$output_delimiter"
cat "$codex_output"
echo "$output_delimiter"
} >> "$GITHUB_OUTPUT"
fi
exit "$codex_status"
exec node "$SCRIPT_DIR/app/main.js"