refactor(entrypoint): 改用 Node.js 主程式執行 action
This commit is contained in:
+3
-2
@@ -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
@@ -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
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user