Merge pull request '處理 AI review findings 並改寫 Node.js entrypoint' (#1) from ai-review-resolve/20260624102518 into develop
Reviewed-on: actions/codex#1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
[
|
||||
{
|
||||
"location": "action.yaml:23",
|
||||
"role": "Assassin",
|
||||
"original_finding": "將包含認證資訊的 `OAUTH` 透過環境變數傳遞,這使得該敏感資訊在容器內的任何進程皆可透過環境變數(如 `/proc/self/environ`)輕易竊取。",
|
||||
"reason": "此 action 透過 Gitea/GitHub Docker Action input 接收 secret,action metadata 只能將 input 映射為容器環境變數;呼叫端仍使用 secrets.CODEX_OAUTH,未在 workflow 明文暴露。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:18",
|
||||
"role": "Assassin",
|
||||
"original_finding": "雖然有 `chmod 600`,但 `auth.json` 放在 `/root/.codex/` 目錄下,若發生容器逃逸,該敏感金鑰極易被讀取。",
|
||||
"reason": "Codex CLI 目前需要讀取 CODEX_HOME/auth.json;容器內已限制為 600 權限,外部 Secret Store 掛載不是此 Docker Action 目前可控制的介面。"
|
||||
},
|
||||
{
|
||||
"location": "action.yaml:6",
|
||||
"role": "Leo",
|
||||
"original_finding": "將 Base64 編碼的認證資訊設為 Action input,導致明文暴露在 Workflow 配置中,難以維護。",
|
||||
"reason": "workflow 傳入的是 secrets.CODEX_OAUTH,不是明文值;使用 input 是 Docker Action 接收呼叫端 secret 的標準介面。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:22",
|
||||
"role": "Rogue",
|
||||
"original_finding": "在腳本中頻繁進行 I/O 操作(重複寫入 auth.json),缺乏快取檢查。",
|
||||
"reason": "每次 Docker Action 執行都是短生命週期容器,auth.json 需要從當次 secret 重建;快取檢查不會降低跨執行 I/O,也可能增加 secret 狀態判斷複雜度。"
|
||||
},
|
||||
{
|
||||
"location": "action.yaml:4",
|
||||
"role": "Bard",
|
||||
"original_finding": "輸入參數 `oauth` 實質為 base64 編碼的 json,命名易生誤解。",
|
||||
"reason": "此 action 已對外發布並由既有 workflow 使用 oauth input;直接更名會造成破壞性變更,先保留相容介面與描述文字。"
|
||||
},
|
||||
{
|
||||
"location": "Dockerfile:4",
|
||||
"role": "Rogue",
|
||||
"original_finding": "在 Dockerfile 中安裝了 git 且未清理,導致映像檔過大。",
|
||||
"reason": "Codex CLI 與 plugin 安裝/執行流程可能需要 git 支援;為避免 runtime 缺少 git 造成 action 失敗,目前保留 git。"
|
||||
},
|
||||
{
|
||||
"location": "Dockerfile:25",
|
||||
"role": "Rogue",
|
||||
"original_finding": "插件安裝指令採取連續序列執行,導致多次獨立的網路 I/O 等待,嚴重浪費建置時間。",
|
||||
"reason": "codex plugin 指令會修改同一份本機 plugin 狀態,並行安裝可能造成狀態競爭;目前保留循序安裝以確保可預測性。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:22",
|
||||
"role": "Leo",
|
||||
"original_finding": "直接使用 `mktemp` 在 `CODEX_HOME` 目錄下建立臨時檔案,且 `CODEX_HOME` 若未正確隔離,在多個 Action 同時執行時可能會導致檔案名稱衝突。",
|
||||
"reason": "Docker Action 每次執行都有獨立容器與 CODEX_HOME;mktemp 也會建立隨機檔名,實務上不會跨 action 執行衝突。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:16",
|
||||
"role": "Leo",
|
||||
"original_finding": "硬編碼了預設路徑 `/root/.codex`,這使得映像檔的可移植性受限,且如果在非 root 使用者環境下執行此容器,可能會因為權限問題而失敗。",
|
||||
"reason": "Dockerfile 明確以 root 環境執行並設定 CODEX_HOME=/root/.codex;entrypoint 仍允許呼叫端用 CODEX_HOME 覆寫預設路徑。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:6",
|
||||
"role": "Maya",
|
||||
"original_finding": "應在測試案例中模擬空 OAUTH 輸入,並驗證腳本是否正確拋出錯誤並以 exit 1 終止。",
|
||||
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:11",
|
||||
"role": "Maya",
|
||||
"original_finding": "應在測試案例中模擬空 MODEL 輸入,並驗證腳本是否正確拋出錯誤並以 exit 1 終止。",
|
||||
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||
},
|
||||
{
|
||||
"location": "Dockerfile:17",
|
||||
"role": "Bard",
|
||||
"original_finding": "將安裝腳本邏輯直接寫在 `RUN` 指令中顯得冗長,且使用 `$(mktemp)` 容易產生難以追蹤的臨時檔案。",
|
||||
"reason": "安裝流程需在同一 Docker layer 中下載、驗證 SHA-256、執行並清理暫存腳本;留在 Dockerfile 可讓供應鏈驗證步驟與安裝命令緊鄰,mktemp 產生的檔案也已於同一 RUN 中刪除。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:45",
|
||||
"role": "Bard",
|
||||
"original_finding": "使用靜態路徑檢查 `auth.json` 是否存在,若前次執行中斷導致檔案未清除,會導致後續執行失敗(Self-inflicted DoS)。",
|
||||
"reason": "拒絕覆寫既有 auth.json 是刻意的 secret 安全保護,避免覆蓋呼叫端掛載或殘留的認證檔;Docker Action 預期每次執行使用獨立容器,前次中斷造成殘留的風險低於靜默覆寫 secret 的風險。"
|
||||
},
|
||||
{
|
||||
"location": "tests/entrypoint_test.sh:17",
|
||||
"role": "Bard",
|
||||
"original_finding": "在測試中建立 `codex` 指令時,Here-document 與命令混雜,可讀性較低。",
|
||||
"reason": "測試 helper 需要動態產生可執行的假 codex 指令,here-document 在此處比額外 fixture 檔更直觀且維持測試自含;此項屬風格偏好,不影響行為正確性。"
|
||||
},
|
||||
{
|
||||
"location": "Dockerfile:10",
|
||||
"role": "Assassin",
|
||||
"original_finding": "Codex 外掛市集來源指向 `gitea.jsc.idv.tw`,未驗證來源的安全性。若該伺服器遭駭,將導致自動安裝惡意或篡改過的外掛,進而導致供應鏈攻擊。",
|
||||
"reason": "Dockerfile 已使用 `codex plugin marketplace add --ref` 將 doc 與 code-review marketplace 固定到指定 commit SHA;此 action 需要安裝內部 Gitea marketplace 外掛,來源伺服器信任與存取控管屬部署環境治理,不適合在 Dockerfile 內改成其他來源。"
|
||||
},
|
||||
{
|
||||
"location": "Dockerfile:24",
|
||||
"role": "Leo",
|
||||
"original_finding": "插件 URL 與版本參照 (REF) 硬編碼在 Dockerfile 中,未來若需更新插件或更換來源,需重新編譯整個 Docker 映像檔,維護成本較高。",
|
||||
"reason": "此 Docker Action 需要可重現的建置結果;將 marketplace URL 與 ref 固定在 Dockerfile ARG 中,可讓外掛版本變更必須經由映像檔重建與 code review,避免外部設定在執行時靜默改變供應鏈內容。"
|
||||
},
|
||||
{
|
||||
"location": "tests/entrypoint_test.sh:65",
|
||||
"role": "Maya",
|
||||
"original_finding": "測試案例對於輸入參數的邊界測試(例如 `PROMPT` 為空字串、極長字串)不足。",
|
||||
"reason": "目前測試已包含 `test_empty_prompt` 與 `test_prompt_with_shell_characters`,覆蓋空字串與含 shell 特殊字元的 PROMPT 傳遞;此 finding 對最新測試內容已不成立。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:49",
|
||||
"role": "Mage",
|
||||
"original_finding": "在執行 install -m 600 時,若 codex 進程已經在嘗試讀取 auth.json,會發生檔案存取競態(Race Condition)。雖然使用了 flock,但這僅在同一個 shell 腳本實例中有效,無法保護跨容器或跨執行環境的檔案存取一致性。",
|
||||
"reason": "最新入口已改為 `entrypoint.sh` 呼叫 `app/main.js`,不存在 `install -m 600`;Node.js 實作會先寫入並 chmod auth.json 後才啟動 codex,且 Docker Action 每次執行為獨立容器,跨容器不共享 CODEX_HOME。"
|
||||
},
|
||||
{
|
||||
"location": "entrypoint.sh:58",
|
||||
"role": "Mage",
|
||||
"original_finding": "在容器化環境(通常是 ephemeral 的)中,auth.json 寫入後立刻被刪除,這會導致 codex 在後續執行中因找不到驗證檔案而無法運作。另外,trap 的清理機制會導致該檔案在 codex 完成工作前被刪除,這對於長效執行或需要多次存取的應用場景是錯誤的設計。",
|
||||
"reason": "最新 Node.js 實作會等待 `codex exec` 子程序結束並寫入 output 後才執行 cleanup;auth.json 在 codex 執行期間持續存在,不會在工作完成前被刪除。"
|
||||
},
|
||||
{
|
||||
"location": "Dockerfile:32",
|
||||
"role": "Mage",
|
||||
"original_finding": "在 Dockerfile 中直接使用 RUN 來下載並執行安裝腳本,沒有進行網路連接穩定性的驗證或完整的錯誤恢復機制。一旦網路不穩導致腳本不完整,後續的 sha256sum 檢查會失敗,但 Dockerfile 層疊技術可能會導致中間層殘留損壞的檔案。",
|
||||
"reason": "Dockerfile 已在同一個 RUN 中完成下載、SHA-256 驗證與安裝,並使用 `curl --retry 3 --retry-delay 2 --max-time 120`;checksum 失敗會使該 layer 建置失敗,不會產生可用的損壞中間成果。"
|
||||
},
|
||||
{
|
||||
"location": "app/main.js:106",
|
||||
"role": "Assassin",
|
||||
"original_finding": "在執行 `codex` 子行程時,使用了 `--dangerously-bypass-approvals-and-sandbox` 參數。這會完全繞過沙盒機制與審核流程,如果 `prompt` 內容受到攻擊者控制,該 CLI 工具將獲得在容器中執行任意代碼的權限。",
|
||||
"reason": "此 action 的用途是在隔離的短生命週期 CI 容器中代替使用者執行 Codex,必須允許 Codex 非互動式修改 workspace;workflow 呼叫端需以 trusted prompt/secret 使用,本次已在 `runCodex` 前加入註解明確標示此安全取捨。"
|
||||
},
|
||||
{
|
||||
"location": "app/main.js:143",
|
||||
"role": "Assassin",
|
||||
"original_finding": "將 `OAUTH` 環境變數內容解碼並直接寫入 `auth.json`。雖然有檢查 base64 格式與 JSON 結構,但若解碼後的 JSON 內容包含惡意配置(如惡意插件路徑或偽造的 API 憑證),可能導致後續 `codex` CLI 在執行時被劫持或洩漏資料。",
|
||||
"reason": "`OAUTH` 是呼叫端提供給 Codex CLI 的 auth.json secret;action 只能驗證 base64、JSON object 與檔案權限,憑證真偽與欄位語意需由 Codex CLI/上游認證機制處理,action 不應猜測或拒絕未來相容欄位。"
|
||||
},
|
||||
{
|
||||
"location": "app/main.js:20",
|
||||
"role": "Leo",
|
||||
"original_finding": "建議至少加上 `console.error` 或在開發/除錯模式下將錯誤拋出,以便在清除失敗時能收到警示。",
|
||||
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||
},
|
||||
{
|
||||
"location": "app/main.js:125",
|
||||
"role": "Mage",
|
||||
"original_finding": "這裡直接使用 `spawn` 執行 `codex` 命令,且參數 `prompt` 是直接從 `process.env.PROMPT` 讀取並傳入的。如果 CI 環境的 `PROMPT` 被惡意竄改,雖使用陣列傳遞參數避免了 shell injection,但 `codex exec` 的邏輯若沒有妥善限制(例如限制可執行指令類型),可能導致攻擊者在 CI Runner 環境執行任意指令。",
|
||||
"reason": "此 action 的目的就是讓呼叫 workflow 以 `PROMPT` 指定 Codex 任務並在隔離 CI 容器內非互動執行;prompt 語意必須由 workflow 與 secret 的信任邊界控管。程式已用 `spawn` 參數陣列避免 shell injection,不適合在 action 內以白名單改寫或限制使用者 prompt。"
|
||||
},
|
||||
{
|
||||
"location": "app/main.js:134",
|
||||
"role": "Maya",
|
||||
"original_finding": "對於 codex 執行失敗的各種細節(權限不足、找不到 binary 等)都統一處理為 status: 1,測試未驗證具體錯誤來源。",
|
||||
"reason": "此 GitHub Action 對外輸出以 `status=completed|failed` 與 `output` 訊息表達成功或失敗;process exit code 只需反映 Codex 子程序結果或通用啟動失敗。細分 ENOENT、EACCES 等 Action exit code 會改變既有介面且對 workflow 判斷價值有限,本次已用簡潔 output 訊息與測試覆蓋具體錯誤來源。"
|
||||
},
|
||||
{
|
||||
"location": "app/main.js:142",
|
||||
"role": "Rogue",
|
||||
"original_finding": "移除這些無謂的重新編碼比較,直接嘗試解碼。",
|
||||
"reason": "AI 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"level": "info",
|
||||
"role": "Bard",
|
||||
"problem": "setupAuth 函式職責過於繁雜。",
|
||||
"suggestion": "將鎖定機制與驗證機制拆分為獨立輔助函式。",
|
||||
"location": "app/main.js:283",
|
||||
"is_new": false
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
jobs:
|
||||
version:
|
||||
name: 計算版本號
|
||||
runs-on: ubuntu
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: 計算版本號
|
||||
id: version
|
||||
uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }}
|
||||
with:
|
||||
IS_BETA: true
|
||||
- name: 標註版本號
|
||||
uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }}
|
||||
with:
|
||||
name: codex v${{ steps.version.outputs.version }}
|
||||
tag_name: v${{ steps.version.outputs.version }}
|
||||
target_commitish: ${{ github.head_ref }}
|
||||
test:
|
||||
name: 工具測試
|
||||
runs-on: ubuntu
|
||||
needs: [version]
|
||||
outputs:
|
||||
status: ${{ steps.test.outputs.status }}
|
||||
output: ${{ steps.test.outputs.output }}
|
||||
steps:
|
||||
- name: 工具測試
|
||||
id: test
|
||||
uses: https://gitea.jsc.idv.tw/actions/codex@v${{ needs.version.outputs.version }}
|
||||
with:
|
||||
oauth: ${{ secrets.CODEX_OAUTH }}
|
||||
model: gpt-5.4-mini
|
||||
- name: 測試失敗
|
||||
if: ${{ steps.test.outputs.status != 'completed' }}
|
||||
run: exit 1
|
||||
@@ -0,0 +1,23 @@
|
||||
name: AI
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- master
|
||||
types: [opened, synchronize]
|
||||
jobs:
|
||||
code-review:
|
||||
name: Code Review
|
||||
runs-on: ubuntu
|
||||
steps:
|
||||
- name: AI Code Review
|
||||
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
|
||||
with:
|
||||
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
|
||||
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
OPENCODE_BASE_URL: ${{ vars.OPENCODE_BASE_URL }}
|
||||
OPENCODE_PROVIDER: ${{ vars.OPENCODE_PROVIDER }}
|
||||
OPENCODE_MODEL: ${{ vars.GEMINI_MODEL }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
+28
-3
@@ -1,10 +1,35 @@
|
||||
FROM alpine:latest
|
||||
|
||||
# 安裝必要的工具
|
||||
RUN apk add --no-cache --no-check-certificate bash
|
||||
# 設定安裝用的環境變數
|
||||
ENV CODEX_NON_INTERACTIVE=1
|
||||
ENV CODEX_INSTALL_DIR=/usr/local/bin
|
||||
ENV CODEX_HOME=/root/.codex
|
||||
ARG CODEX_INSTALL_SHA256=73eb367137d151eabe89590a1d1a08a9c2fe2800a656bffac3c5707b62b42683
|
||||
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 nodejs
|
||||
|
||||
# 安裝 Codex CLI 工具
|
||||
RUN install_script="$(mktemp)" \
|
||||
&& trap 'rm -f "$install_script"' EXIT \
|
||||
&& curl -fsSL --retry 3 --retry-delay 2 --max-time 120 \
|
||||
https://chatgpt.com/codex/install.sh \
|
||||
-o "$install_script" \
|
||||
&& echo "${CODEX_INSTALL_SHA256} ${install_script}" | sha256sum -c - \
|
||||
&& sh "$install_script" \
|
||||
&& codex --version
|
||||
|
||||
# 安裝技能
|
||||
RUN codex plugin marketplace add "https://gitea.jsc.idv.tw/plugins/doc.git" --ref "$CODEX_DOC_MARKETPLACE_REF" \
|
||||
&& codex plugin marketplace add "https://gitea.jsc.idv.tw/plugins/code-review.git" --ref "$CODEX_CODE_REVIEW_MARKETPLACE_REF" \
|
||||
&& 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"]
|
||||
+18
-12
@@ -1,20 +1,26 @@
|
||||
name: 'Docker Action Template'
|
||||
description: 'Docker Action 範本'
|
||||
name: 'Codex CLI'
|
||||
description: 'Codex CLI Action'
|
||||
author: 'Jeffery'
|
||||
inputs:
|
||||
runner_token:
|
||||
description: 'Gitea Runner Token'
|
||||
oauth:
|
||||
description: 'Base64 encoded Codex auth.json'
|
||||
required: true
|
||||
text:
|
||||
description: '輸入的文字'
|
||||
default: "Hello, World!"
|
||||
model:
|
||||
description: 'Codex model name'
|
||||
required: true
|
||||
prompt:
|
||||
description: 'Prompt for codex exec'
|
||||
required: false
|
||||
default: '請自我介紹'
|
||||
outputs:
|
||||
text:
|
||||
description: '輸出的文字'
|
||||
status:
|
||||
description: 'Execution result status'
|
||||
output:
|
||||
description: 'Codex execution output'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
env:
|
||||
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
RUNNER_TOKEN: ${{ inputs.runner_token || secrets.GITEA_TOKEN || secrets.RUNNER_TOKEN }}
|
||||
OAUTH: ${{ inputs.oauth }}
|
||||
MODEL: ${{ inputs.model }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const DEFAULT_PROMPT = "請自我介紹";
|
||||
const FILE_MODE_PRIVATE = 0o600;
|
||||
const DIR_MODE_PRIVATE = 0o700;
|
||||
const DEFAULT_CODEX_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
|
||||
const TEMP_DIR_PREFIX = ".codex-action-";
|
||||
const OUTPUT_DELIMITER_PREFIX = "CODEX_OUTPUT_";
|
||||
|
||||
class TempFileRegistry {
|
||||
constructor() {
|
||||
this.files = new Set();
|
||||
this.dirs = new Set();
|
||||
}
|
||||
|
||||
trackFile(filePath) {
|
||||
this.files.add(filePath);
|
||||
}
|
||||
|
||||
trackDir(dirPath) {
|
||||
this.dirs.add(dirPath);
|
||||
}
|
||||
|
||||
removeFile(filePath) {
|
||||
if (!filePath || !this.files.has(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
this.files.delete(filePath);
|
||||
} catch (error) {
|
||||
console.error(`Unable to remove temporary file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for (const filePath of this.files) {
|
||||
this.removeFile(filePath);
|
||||
}
|
||||
|
||||
for (const dirPath of this.dirs) {
|
||||
try {
|
||||
fs.rmSync(dirPath, { force: true, recursive: true });
|
||||
this.dirs.delete(dirPath);
|
||||
} catch (error) {
|
||||
console.error(`Unable to remove temporary directory: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tempFiles = new TempFileRegistry();
|
||||
|
||||
class OutputCollector {
|
||||
constructor(maxBytes) {
|
||||
this.maxBytes = maxBytes;
|
||||
this.chunks = [];
|
||||
this.size = 0;
|
||||
this.truncated = false;
|
||||
}
|
||||
|
||||
append(chunk) {
|
||||
const available = this.maxBytes - this.size;
|
||||
|
||||
if (available <= 0) {
|
||||
this.truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const storedChunk = chunk.length > available ? chunk.subarray(0, available) : chunk;
|
||||
this.chunks.push(storedChunk);
|
||||
this.size += storedChunk.length;
|
||||
|
||||
if (storedChunk.length < chunk.length) {
|
||||
this.truncated = true;
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
const truncationMessage = this.truncated ? "\n[Output truncated]\n" : "";
|
||||
return `${Buffer.concat(this.chunks, this.size).toString()}${truncationMessage}`;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
tempFiles.cleanup();
|
||||
}
|
||||
|
||||
function makeTempDir(dir) {
|
||||
const tempDir = fs.mkdtempSync(path.join(dir, TEMP_DIR_PREFIX));
|
||||
fs.chmodSync(tempDir, DIR_MODE_PRIVATE);
|
||||
tempFiles.trackDir(tempDir);
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
function makeTempFile(dir, prefix) {
|
||||
const random = crypto.randomBytes(16).toString("hex");
|
||||
const filePath = path.join(dir, `${prefix}.${random}`);
|
||||
const fd = fs.openSync(filePath, "wx", FILE_MODE_PRIVATE);
|
||||
fs.closeSync(fd);
|
||||
tempFiles.trackFile(filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function appendGithubOutput(status, output) {
|
||||
const outputFile = process.env.GITHUB_OUTPUT;
|
||||
if (!outputFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
let delimiter;
|
||||
do {
|
||||
delimiter = `${OUTPUT_DELIMITER_PREFIX}${crypto.randomBytes(12).toString("hex")}`;
|
||||
} while (output.includes(delimiter));
|
||||
|
||||
fs.appendFileSync(
|
||||
outputFile,
|
||||
`status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`,
|
||||
{ encoding: "utf8", mode: FILE_MODE_PRIVATE },
|
||||
);
|
||||
}
|
||||
|
||||
function fail(message, code = 1) {
|
||||
console.error(message);
|
||||
appendGithubOutput("failed", message);
|
||||
cleanup();
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function compactBase64(value) {
|
||||
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) {
|
||||
if (!isBase64(encodedAuth)) {
|
||||
fail("OAUTH must be valid base64 encoded Codex auth.json.");
|
||||
}
|
||||
|
||||
const decoded = Buffer.from(compactBase64(encodedAuth), "base64");
|
||||
|
||||
fs.writeFileSync(authFile, decoded, { mode: FILE_MODE_PRIVATE });
|
||||
|
||||
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 parsePositiveInteger(value, fallback) {
|
||||
const parsed = Number.parseInt(value || "", 10);
|
||||
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) {
|
||||
return new Promise((resolve) => {
|
||||
const { timeoutMs, outputLimitBytes, workspace } = readExecutionConfig();
|
||||
const args = codexExecArgs(model, prompt);
|
||||
|
||||
// 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("codex", args, { cwd: workspace, stdio: ["ignore", "pipe", "pipe"] });
|
||||
|
||||
const output = new OutputCollector(outputLimitBytes);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
child.kill("SIGTERM");
|
||||
output.append(Buffer.from(`Codex execution timed out after ${timeoutMs} ms.\n`));
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.pipe(process.stdout);
|
||||
child.stderr.pipe(process.stdout);
|
||||
|
||||
child.stdout.on("data", (chunk) => {
|
||||
output.append(chunk);
|
||||
});
|
||||
|
||||
child.stderr.on("data", (chunk) => {
|
||||
output.append(chunk);
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
const message = error.code === "ENOENT" ? "Unable to find codex command.\n" : `${error.message}\n`;
|
||||
output.append(Buffer.from(message));
|
||||
resolve({ status: 1, output: output.toString() });
|
||||
});
|
||||
|
||||
child.on("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (code === null && signal) {
|
||||
output.append(Buffer.from(`Codex process terminated by signal ${signal}.\n`));
|
||||
}
|
||||
|
||||
resolve({ status: code ?? 1, output: output.toString() });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerCleanupHandlers() {
|
||||
process.on("exit", cleanup);
|
||||
process.on("SIGINT", () => {
|
||||
cleanup();
|
||||
process.exit(130);
|
||||
});
|
||||
process.on("SIGTERM", () => {
|
||||
cleanup();
|
||||
process.exit(143);
|
||||
});
|
||||
}
|
||||
|
||||
function readConfig() {
|
||||
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.");
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE });
|
||||
fs.chmodSync(codexHome, DIR_MODE_PRIVATE);
|
||||
fs.accessSync(codexHome, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
|
||||
} catch {
|
||||
fail("Unable to create CODEX_HOME.");
|
||||
}
|
||||
|
||||
const tempDir = makeTempDir(codexHome);
|
||||
const authFile = makeTempFile(tempDir, "auth");
|
||||
const authPath = path.join(codexHome, "auth.json");
|
||||
const lockHandle = createAuthLock(codexHome);
|
||||
|
||||
validateAuth(oauth, authFile);
|
||||
|
||||
if (fs.existsSync(authPath)) {
|
||||
fail("Refusing to overwrite existing Codex auth.json.");
|
||||
}
|
||||
|
||||
fs.renameSync(authFile, authPath);
|
||||
fs.chmodSync(authPath, FILE_MODE_PRIVATE);
|
||||
tempFiles.trackFile(authPath);
|
||||
|
||||
return lockHandle;
|
||||
}
|
||||
|
||||
async function runCodexAction({ oauth, model, codexHome, prompt }) {
|
||||
const lockHandle = setupAuth(oauth, codexHome);
|
||||
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);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
registerCleanupHandlers();
|
||||
await runCodexAction(readConfig());
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
+3
-7
@@ -1,11 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Gitea Server Url: $GITEA_SERVER_URL"
|
||||
set -euo pipefail
|
||||
|
||||
echo "Gitea Repository: $GITEA_REPOSITORY"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "Gitea Runner Token: $RUNNER_TOKEN"
|
||||
|
||||
echo "Input Text: $INPUT_TEXT"
|
||||
|
||||
echo "text=$INPUT_TEXT" >> "$GITHUB_OUTPUT"
|
||||
exec node "$SCRIPT_DIR/app/main.js"
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
IMAGE_NAME="codex-action-smoke:local"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "SKIP: docker is not available"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "SKIP: docker daemon is not available"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker build -t "$IMAGE_NAME" "$ROOT_DIR"
|
||||
|
||||
docker run --rm --entrypoint codex "$IMAGE_NAME" --version
|
||||
docker run --rm --entrypoint sh "$IMAGE_NAME" -c 'mkdir -p "$CODEX_HOME" && test -w "$CODEX_HOME" && codex --version >/dev/null'
|
||||
docker run --rm --entrypoint codex "$IMAGE_NAME" plugin list | grep -q "jsc@doc"
|
||||
docker run --rm --entrypoint codex "$IMAGE_NAME" plugin list | grep -q "jsc@code-review"
|
||||
Executable
+304
@@ -0,0 +1,304 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
die() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
make_fake_codex() {
|
||||
local dir="$1"
|
||||
local exit_code="$2"
|
||||
local message="$3"
|
||||
|
||||
cat > "$dir/codex" <<SH
|
||||
#!/bin/sh
|
||||
printf '%s\n' "\$*" > "\${CODEX_FAKE_ARGS:-/dev/null}"
|
||||
echo "$message"
|
||||
exit $exit_code
|
||||
SH
|
||||
chmod +x "$dir/codex"
|
||||
}
|
||||
|
||||
make_fake_sleeping_codex() {
|
||||
local dir="$1"
|
||||
|
||||
cat > "$dir/codex" <<'SH'
|
||||
#!/bin/sh
|
||||
trap 'exit 143' TERM
|
||||
while true; do
|
||||
sleep 1
|
||||
done
|
||||
SH
|
||||
chmod +x "$dir/codex"
|
||||
}
|
||||
|
||||
make_fake_signaled_codex() {
|
||||
local dir="$1"
|
||||
|
||||
cat > "$dir/codex" <<'SH'
|
||||
#!/bin/sh
|
||||
kill -TERM $$
|
||||
SH
|
||||
chmod +x "$dir/codex"
|
||||
}
|
||||
|
||||
make_fake_large_output_codex() {
|
||||
local dir="$1"
|
||||
|
||||
cat > "$dir/codex" <<'SH'
|
||||
#!/bin/sh
|
||||
printf '0123456789'
|
||||
SH
|
||||
chmod +x "$dir/codex"
|
||||
}
|
||||
|
||||
encoded_json() {
|
||||
printf '%s' "$1" | base64 | tr -d '\n'
|
||||
}
|
||||
|
||||
run_entrypoint() {
|
||||
local tmpdir="$1"
|
||||
shift
|
||||
|
||||
env \
|
||||
PATH="$tmpdir:$PATH" \
|
||||
CODEX_HOME="$tmpdir/codex-home" \
|
||||
GITHUB_OUTPUT="$tmpdir/github-output" \
|
||||
"$@" \
|
||||
bash "$ROOT_DIR/entrypoint.sh" > "$tmpdir/stdout" 2> "$tmpdir/stderr"
|
||||
}
|
||||
|
||||
assert_status() {
|
||||
local actual="$1"
|
||||
local expected="$2"
|
||||
|
||||
[[ "$actual" -eq "$expected" ]] || die "expected status $expected, got $actual"
|
||||
}
|
||||
|
||||
test_missing_oauth() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_codex "$tmpdir" 0 "unused"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" MODEL="gpt-test" PROMPT="hello"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 1
|
||||
grep -q "OAUTH is required" "$tmpdir/stderr" || die "missing OAUTH error"
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "missing OAUTH failed status output"
|
||||
grep -q "OAUTH is required" "$tmpdir/github-output" || die "missing OAUTH output"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_missing_model() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_codex "$tmpdir" 0 "unused"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" PROMPT="hello"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 1
|
||||
grep -q "MODEL is required" "$tmpdir/stderr" || die "missing MODEL error"
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "missing MODEL failed status output"
|
||||
grep -q "MODEL is required" "$tmpdir/github-output" || die "missing MODEL output"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_invalid_base64() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_codex "$tmpdir" 0 "unused"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" OAUTH="not-base64" MODEL="gpt-test" PROMPT="hello"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 1
|
||||
grep -q "valid base64" "$tmpdir/stderr" || die "invalid base64 error"
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "invalid base64 failed status output"
|
||||
grep -q "valid base64" "$tmpdir/github-output" || die "invalid base64 output"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_non_object_json() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_codex "$tmpdir" 0 "unused"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '[]')" MODEL="gpt-test" PROMPT="hello"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 1
|
||||
grep -q "JSON object" "$tmpdir/stderr" || die "non-object JSON error"
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "non-object JSON failed status output"
|
||||
grep -q "JSON object" "$tmpdir/github-output" || die "non-object JSON output"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_success_output() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_codex "$tmpdir" 0 "codex ok"
|
||||
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||
|
||||
grep -q "status=completed" "$tmpdir/github-output" || die "completed status output"
|
||||
grep -q "codex ok" "$tmpdir/github-output" || die "codex output"
|
||||
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_empty_prompt() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_codex "$tmpdir" 0 "codex ok"
|
||||
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT=""
|
||||
|
||||
grep -q "codex ok" "$tmpdir/github-output" || die "empty prompt output"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_prompt_with_shell_characters() {
|
||||
local tmpdir
|
||||
local prompt
|
||||
tmpdir="$(mktemp -d)"
|
||||
prompt='hello; rm -rf / $(echo bad) "quoted"'
|
||||
make_fake_codex "$tmpdir" 0 "codex ok"
|
||||
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="$prompt" CODEX_FAKE_ARGS="$tmpdir/codex-args"
|
||||
|
||||
grep -qF "$prompt" "$tmpdir/codex-args" || die "prompt was not passed as one argument"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_failure_output() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_codex "$tmpdir" 7 "codex failed"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 7
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "failed status output"
|
||||
grep -q "codex failed" "$tmpdir/github-output" || die "failed codex output"
|
||||
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up after failure"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_missing_codex_command_output() {
|
||||
local tmpdir
|
||||
local bindir
|
||||
tmpdir="$(mktemp -d)"
|
||||
bindir="$tmpdir/bin"
|
||||
mkdir -p "$bindir"
|
||||
ln -s "$(command -v bash)" "$bindir/bash"
|
||||
ln -s "$(command -v dirname)" "$bindir/dirname"
|
||||
ln -s "$(command -v node)" "$bindir/node"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" PATH="$bindir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 1
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "missing codex command failed status output"
|
||||
grep -q "Unable to find codex command" "$tmpdir/github-output" || die "missing codex command error output"
|
||||
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up after missing codex"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_codex_timeout_output() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_sleeping_codex "$tmpdir"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello" CODEX_TIMEOUT_MS="100"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 143
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "timeout failed status output"
|
||||
grep -q "Codex execution timed out after 100 ms" "$tmpdir/github-output" || die "timeout message output"
|
||||
[[ ! -e "$tmpdir/codex-home/auth.json" ]] || die "auth.json was not cleaned up after timeout"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_codex_signal_output() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_signaled_codex "$tmpdir"
|
||||
|
||||
set +e
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello"
|
||||
local status="$?"
|
||||
set -e
|
||||
|
||||
assert_status "$status" 1
|
||||
grep -q "status=failed" "$tmpdir/github-output" || die "signal failed status output"
|
||||
grep -q "Codex process terminated by signal SIGTERM" "$tmpdir/github-output" || die "signal message output"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_codex_output_truncation() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_large_output_codex "$tmpdir"
|
||||
|
||||
run_entrypoint "$tmpdir" OAUTH="$(encoded_json '{}')" MODEL="gpt-test" PROMPT="hello" CODEX_OUTPUT_LIMIT_BYTES="5"
|
||||
|
||||
grep -q "status=completed" "$tmpdir/github-output" || die "truncated output completed status"
|
||||
grep -q "01234" "$tmpdir/github-output" || die "truncated output prefix"
|
||||
grep -q "\\[Output truncated\\]" "$tmpdir/github-output" || die "truncation message output"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_invalid_execution_config_uses_defaults() {
|
||||
local tmpdir
|
||||
tmpdir="$(mktemp -d)"
|
||||
make_fake_large_output_codex "$tmpdir"
|
||||
|
||||
run_entrypoint "$tmpdir" \
|
||||
OAUTH="$(encoded_json '{}')" \
|
||||
MODEL="gpt-test" \
|
||||
PROMPT="hello" \
|
||||
CODEX_TIMEOUT_MS="invalid" \
|
||||
CODEX_OUTPUT_LIMIT_BYTES="invalid"
|
||||
|
||||
grep -q "status=completed" "$tmpdir/github-output" || die "invalid config completed status"
|
||||
grep -q "0123456789" "$tmpdir/github-output" || die "invalid output limit did not use default"
|
||||
! grep -q "\\[Output truncated\\]" "$tmpdir/github-output" || die "invalid output limit unexpectedly truncated"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
test_missing_oauth
|
||||
test_missing_model
|
||||
test_invalid_base64
|
||||
test_non_object_json
|
||||
test_success_output
|
||||
test_empty_prompt
|
||||
test_prompt_with_shell_characters
|
||||
test_failure_output
|
||||
test_missing_codex_command_output
|
||||
test_codex_timeout_output
|
||||
test_codex_signal_output
|
||||
test_codex_output_truncation
|
||||
test_invalid_execution_config_uses_defaults
|
||||
|
||||
echo "entrypoint tests passed"
|
||||
Reference in New Issue
Block a user