處理 AI review findings 並改寫 Node.js entrypoint #2
+3
-2
@@ -9,7 +9,7 @@ ARG CODEX_DOC_MARKETPLACE_REF=f8da961328a85f267b4402566e127310370169da
|
|||||||
ARG CODEX_CODE_REVIEW_MARKETPLACE_REF=9e016edff016d58f4d64e0a5468220d35a0f657b
|
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 工具
|
# 安裝 Codex CLI 工具
|
||||||
RUN install_script="$(mktemp)" \
|
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@doc" \
|
||||||
&& codex plugin add "jsc@code-review"
|
&& codex plugin add "jsc@code-review"
|
||||||
|
|
||||||
|
COPY app /app
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh /app/main.js
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|||||||
+193
@@ -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
@@ -1,85 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -eo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
die() {
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
echo "$1" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
exec node "$SCRIPT_DIR/app/main.js"
|
||||||
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"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user