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

Merged
jiantw83 merged 58 commits from ai-review-resolve/20260624102518 into develop 2026-06-24 14:09:27 +00:00
10 changed files with 939 additions and 23 deletions
+158
View File
@@ -0,0 +1,158 @@
[
{
"location": "action.yaml:23",
"role": "Assassin",
"original_finding": "將包含認證資訊的 `OAUTH` 透過環境變數傳遞,這使得該敏感資訊在容器內的任何進程皆可透過環境變數(如 `/proc/self/environ`)輕易竊取。",
"reason": "此 action 透過 Gitea/GitHub Docker Action input 接收 secretaction 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/.codexentrypoint 仍允許呼叫端用 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 後才執行 cleanupauth.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 非互動式修改 workspaceworkflow 呼叫端需以 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 secretaction 只能驗證 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 對話收斂判定為誤報(問題在最新程式碼中不成立或不適用)"
}
]
+10
View File
@@ -0,0 +1,10 @@
[
{
"level": "info",
"role": "Bard",
"problem": "setupAuth 函式職責過於繁雜。",
"suggestion": "將鎖定機制與驗證機制拆分為獨立輔助函式。",
"location": "app/main.js:283",
"is_new": false
}
]
+39
View File
@@ -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
+23
View File
@@ -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
View File
@@ -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
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Assassin
問題:Codex 外掛市集來源指向 gitea.jsc.idv.tw,未驗證來源的安全性。若該伺服器遭駭,將導致自動安裝惡意或篡改過的外掛,進而導致供應鏈攻擊。
建議:若可能,請將外掛來源固定在受信任的內部儲存庫或使用經簽署的外掛版本。確保來源伺服器具有嚴格的存取控管與安全性掃描。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:Codex 外掛市集來源指向 `gitea.jsc.idv.tw`,未驗證來源的安全性。若該伺服器遭駭,將導致自動安裝惡意或篡改過的外掛,進而導致供應鏈攻擊。 **建議**:若可能,請將外掛來源固定在受信任的內部儲存庫或使用經簽署的外掛版本。確保來源伺服器具有嚴格的存取控管與安全性掃描。
Review

嚴重等級🟡 警告
審查員:Leo
問題:環境變數使用以空格分隔的字串,若未來名稱中包含空格將導致 shell 展開錯誤,且難以維護。
建議:建議改用換行符號(
)分隔,並在安裝迴圈中使用 IFS=$' ' 處理,以提高 shell 指令的健壯性。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:環境變數使用以空格分隔的字串,若未來名稱中包含空格將導致 shell 展開錯誤,且難以維護。 **建議**:建議改用換行符號( )分隔,並在安裝迴圈中使用 `IFS=$' '` 處理,以提高 shell 指令的健壯性。
# 安裝必要的工具
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 \
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Leo
問題:在 Dockerfile 中直接使用 curl 下載並執行安裝腳本 (sh "$install_script") 存在潛在的安全風險與不確定性,且沒有檢查腳本的完整性或簽章。若腳本內容在未來變更,可能導致映像檔建置失敗或植入非預期的內容,增加維護與安全風險。
建議:建議將安裝腳本改為明確的版本化下載,或者如果可能,將安裝邏輯整合進 Dockerfile 自身,以確保建置過程的冪等性與安全性。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:在 Dockerfile 中直接使用 `curl` 下載並執行安裝腳本 (`sh "$install_script"`) 存在潛在的安全風險與不確定性,且沒有檢查腳本的完整性或簽章。若腳本內容在未來變更,可能導致映像檔建置失敗或植入非預期的內容,增加維護與安全風險。 **建議**:建議將安裝腳本改為明確的版本化下載,或者如果可能,將安裝邏輯整合進 Dockerfile 自身,以確保建置過程的冪等性與安全性。
Review

嚴重等級🟡 警告
審查員:Leo
問題:在 Dockerfile 中直接使用 curl 下載並執行安裝腳本存在潛在的安全風險與不確定性,且沒有檢查腳本的完整性或簽章。若腳本內容在未來變更,可能導致映像檔建置失敗或植入非預期的內容。
建議:建議將安裝腳本改為明確的版本化下載,或者將安裝邏輯整合進 Dockerfile 自身。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:在 Dockerfile 中直接使用 `curl` 下載並執行安裝腳本存在潛在的安全風險與不確定性,且沒有檢查腳本的完整性或簽章。若腳本內容在未來變更,可能導致映像檔建置失敗或植入非預期的內容。 **建議**:建議將安裝腳本改為明確的版本化下載,或者將安裝邏輯整合進 Dockerfile 自身。
Review

嚴重等級🔴 嚴重
審查員:Assassin
問題:Dockerfile 直接從外部連結下載並執行腳本,且未驗證其雜湊值,若來源遭攔截或篡改,將導致任意程式碼執行。
建議:下載指令碼後,務必使用 sha256sum 或其他雜湊函數驗證其完整性,確保與預期內容一致後再執行。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:Dockerfile 直接從外部連結下載並執行腳本,且未驗證其雜湊值,若來源遭攔截或篡改,將導致任意程式碼執行。 **建議**:下載指令碼後,務必使用 sha256sum 或其他雜湊函數驗證其完整性,確保與預期內容一致後再執行。
Review

嚴重等級🟡 警告
審查員:Bard
問題:將安裝腳本邏輯直接寫在 RUN 指令中顯得冗長,且使用 $(mktemp) 容易產生難以追蹤的臨時檔案。
建議:考慮將安裝邏輯封裝成一個獨立的 script 檔案,讓 Dockerfile 更簡潔優雅。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:將安裝腳本邏輯直接寫在 `RUN` 指令中顯得冗長,且使用 `$(mktemp)` 容易產生難以追蹤的臨時檔案。 **建議**:考慮將安裝邏輯封裝成一個獨立的 script 檔案,讓 `Dockerfile` 更簡潔優雅。
https://chatgpt.com/codex/install.sh \
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Mage
問題:若 sh "$install_script" 安裝過程失敗,rm -f 由於被 && 連接,不會被執行,導致 build 過程中殘留不必要的暫存檔案,雖然 Docker Layer 會自動清除,但仍屬不佳的清理實作。
建議:建議改用 trap 進行清理,或確保 rm -f 指令在 failure 情境下也能執行,例如:sh "$install_script" || { rm -f "$install_script"; exit 1; }

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:若 sh "$install_script" 安裝過程失敗,rm -f 由於被 && 連接,不會被執行,導致 build 過程中殘留不必要的暫存檔案,雖然 Docker Layer 會自動清除,但仍屬不佳的清理實作。 **建議**:建議改用 trap 進行清理,或確保 rm -f 指令在 failure 情境下也能執行,例如:sh "$install_script" || { rm -f "$install_script"; exit 1; }
-o "$install_script" \
&& echo "${CODEX_INSTALL_SHA256} ${install_script}" | sha256sum -c - \
&& sh "$install_script" \
&& codex --version
# 安裝技能
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Leo
問題:插件 URL 與版本參照 (REF) 硬編碼在 Dockerfile 中,未來若需更新插件或更換來源,需重新編譯整個 Docker 映像檔,維護成本較高。
建議:建議將這些插件清單與版本資訊移至外部設定檔 (如 plugins.json),並在 Dockerfile 中讀取該檔案進行安裝,增加彈性。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:插件 URL 與版本參照 (REF) 硬編碼在 Dockerfile 中,未來若需更新插件或更換來源,需重新編譯整個 Docker 映像檔,維護成本較高。 **建議**:建議將這些插件清單與版本資訊移至外部設定檔 (如 plugins.json),並在 Dockerfile 中讀取該檔案進行安裝,增加彈性。
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
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 Dockerfile 中直接使用 RUN 來下載並執行安裝腳本,沒有進行網路連接穩定性的驗證或完整的錯誤恢復機制。一旦網路不穩導致腳本不完整,後續的 sha256sum 檢查會失敗,但 Dockerfile 層疊技術可能會導致中間層殘留損壞的檔案。
建議:將下載、SHA256 驗證與安裝合併在同一個 RUN 指令中,並加入重試機制(如已有的 --retry),確保每一層的原子性。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 Dockerfile 中直接使用 RUN 來下載並執行安裝腳本,沒有進行網路連接穩定性的驗證或完整的錯誤恢復機制。一旦網路不穩導致腳本不完整,後續的 sha256sum 檢查會失敗,但 Dockerfile 層疊技術可能會導致中間層殘留損壞的檔案。 **建議**:將下載、SHA256 驗證與安裝合併在同一個 RUN 指令中,並加入重試機制(如已有的 --retry),確保每一層的原子性。
RUN chmod +x /entrypoint.sh
RUN chmod +x /entrypoint.sh /app/main.js
ENTRYPOINT ["/entrypoint.sh"]
+18 -12
View File
@@ -1,20 +1,26 @@
name: 'Docker Action Template'
description: 'Docker Action 範本'
name: 'Codex CLI'
description: 'Codex CLI Action'
author: 'Jeffery'
inputs:
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Bard
問題:輸入參數 oauth 實質為 base64 編碼的 json,命名易生誤解。
建議:建議更名為 auth_configencoded_auth_json

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:輸入參數 `oauth` 實質為 base64 編碼的 json,命名易生誤解。 **建議**:建議更名為 `auth_config` 或 `encoded_auth_json`。
runner_token:
description: 'Gitea Runner Token'
oauth:
description: 'Base64 encoded Codex auth.json'
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Leo
問題:將 Base64 編碼的認證資訊設為 Action input,導致明文暴露在 Workflow 配置中,難以維護。
建議:應改為直接使用 Action 的 Secrets 機制(如 secrets.CODEX_AUTH),將認證資訊與設定分離。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:將 Base64 編碼的認證資訊設為 Action input,導致明文暴露在 Workflow 配置中,難以維護。 **建議**:應改為直接使用 Action 的 Secrets 機制(如 `secrets.CODEX_AUTH`),將認證資訊與設定分離。
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'
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Bard
問題:輸出欄位命名為 text,但賦值內容為「執行狀態(status)」,語義不符。
建議:將欄位名稱改為 status,以保持命名與意圖一致。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:輸出欄位命名為 `text`,但賦值內容為「執行狀態(status)」,語義不符。 **建議**:將欄位名稱改為 `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
View File
@@ -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;
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Bard
問題:全域變數 createdPaths 在檔案層級被宣告,讓函式產生強依賴,缺乏封裝性,讀起來不夠優雅。
建議:將臨時檔案管理邏輯封裝成一個類別(如 TempFileRegistry),讓狀態更具備物件導向的封裝性。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:全域變數 `createdPaths` 在檔案層級被宣告,讓函式產生強依賴,缺乏封裝性,讀起來不夠優雅。 **建議**:將臨時檔案管理邏輯封裝成一個類別(如 `TempFileRegistry`),讓狀態更具備物件導向的封裝性。
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();
}
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Leo
問題:在 removeIfCreated 函式中靜默捕捉錯誤 (catch { ... }),這會遮蔽潛在的權限或檔案系統問題,使除錯困難。
建議:建議至少加上 console.error 或在開發/除錯模式下將錯誤拋出,以便在清除失敗時能收到警示。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:在 `removeIfCreated` 函式中靜默捕捉錯誤 (`catch { ... }`),這會遮蔽潛在的權限或檔案系統問題,使除錯困難。 **建議**:建議至少加上 `console.error` 或在開發/除錯模式下將錯誤拋出,以便在清除失敗時能收到警示。
trackFile(filePath) {
this.files.add(filePath);
}
trackDir(dirPath) {
this.dirs.add(dirPath);
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Rogue
問題:在 makeTempFile 中,使用了 Math.random().toString(16).slice(2) 來生成隨機檔名。對於高頻率呼叫的場景,這會產生不必要的計算開銷與效能損耗。
建議:建議使用 Node.js 內建的 crypto.randomBytescrypto.randomUUID,雖然效能略有差異但更具安全性與標準化,且能減少字串轉換次數。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在 `makeTempFile` 中,使用了 `Math.random().toString(16).slice(2)` 來生成隨機檔名。對於高頻率呼叫的場景,這會產生不必要的計算開銷與效能損耗。 **建議**:建議使用 Node.js 內建的 `crypto.randomBytes` 或 `crypto.randomUUID`,雖然效能略有差異但更具安全性與標準化,且能減少字串轉換次數。
}
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Bard
問題:隨機檔案名稱的生成邏輯過於冗長且複雜,破壞了程式碼的簡潔美感。
建議:建議使用 Node.js 原生的 crypto 模組,例如 crypto.randomBytes(16).toString('hex'),讓產生的字串更優雅、清晰。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:隨機檔案名稱的生成邏輯過於冗長且複雜,破壞了程式碼的簡潔美感。 **建議**:建議使用 Node.js 原生的 `crypto` 模組,例如 `crypto.randomBytes(16).toString('hex')`,讓產生的字串更優雅、清晰。
removeFile(filePath) {
if (!filePath || !this.files.has(filePath)) {
return;
}
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 makeTempFile 中使用了 fs.openSync(filePath, "wx", 0o600)。如果在 fs.closeSync(fd) 之前程式因例外或強制終止(SIGKILL),該檔案會留在硬碟上直到下次清理或手動刪除,且其檔案描述子會持續開啟直到 process 結束。
建議:建議使用 fs.mkdtempSync 建立獨立目錄,將所有臨時檔案放入該目錄,並在 cleanup 時直接移除整個目錄,以確保清理的原子性與完整性。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `makeTempFile` 中使用了 `fs.openSync(filePath, "wx", 0o600)`。如果在 `fs.closeSync(fd)` 之前程式因例外或強制終止(SIGKILL),該檔案會留在硬碟上直到下次清理或手動刪除,且其檔案描述子會持續開啟直到 process 結束。 **建議**:建議使用 `fs.mkdtempSync` 建立獨立目錄,將所有臨時檔案放入該目錄,並在 `cleanup` 時直接移除整個目錄,以確保清理的原子性與完整性。
Review

嚴重等級🔵 建議
審查員:Bard
問題:檔案權限(如 0o600, 0o700)以數字字面量多次出現,散落在程式碼中,降低了可讀性與一致性。
建議:在檔案上方定義權限常數(例如 const FILE_MODE_PRIVATE = 0o600;),讓語義更清晰。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:檔案權限(如 `0o600`, `0o700`)以數字字面量多次出現,散落在程式碼中,降低了可讀性與一致性。 **建議**:在檔案上方定義權限常數(例如 `const FILE_MODE_PRIVATE = 0o600;`),讓語義更清晰。
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);
}
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Rogue
問題:在 TempFileRegistrycleanup 方法中,每次呼叫都使用 Array.from 將 Set 轉換為陣列,這在頻繁清理時會產生無謂的記憶體開銷。
建議:若無強烈反向迭代的需求,可考慮直接使用 forEach 遍歷 Set。若有嚴格順序需求,建議改用其他結構管理,避免每次 cleanup 都額外配置陣列。

**嚴重等級**:🔵 建議 **審查員**:Rogue **問題**:在 `TempFileRegistry` 的 `cleanup` 方法中,每次呼叫都使用 `Array.from` 將 Set 轉換為陣列,這在頻繁清理時會產生無謂的記憶體開銷。 **建議**:若無強烈反向迭代的需求,可考慮直接使用 `forEach` 遍歷 Set。若有嚴格順序需求,建議改用其他結構管理,避免每次 cleanup 都額外配置陣列。
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) {
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Rogue
問題:在 validateAuth 中,為了驗證 base64 字串是否符合 base64 格式,進行了多次正規表達式替換與編解碼運算(如 encodedAuth.replaceBuffer.fromdecoded.toString('base64') 等),這在每次執行都會發生的情況下,浪費了不必要的 CPU 週期。
建議:如果目的只是驗證結構,建議盡量簡化邏輯。可以直接將字串嘗試轉換為 Buffer 並檢查 toString('base64') 是否匹配,避免多重正規表達式替換。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在 `validateAuth` 中,為了驗證 base64 字串是否符合 base64 格式,進行了多次正規表達式替換與編解碼運算(如 `encodedAuth.replace`、`Buffer.from`、`decoded.toString('base64')` 等),這在每次執行都會發生的情況下,浪費了不必要的 CPU 週期。 **建議**:如果目的只是驗證結構,建議盡量簡化邏輯。可以直接將字串嘗試轉換為 Buffer 並檢查 `toString('base64')` 是否匹配,避免多重正規表達式替換。
this.maxBytes = maxBytes;
this.chunks = [];
this.size = 0;
this.truncated = false;
}
append(chunk) {
const available = this.maxBytes - this.size;
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Bard
問題:Base64 的驗證過程充滿了複雜的字串正規化與取代操作,讀起來像是在解迷宮,而非驗證身分。
建議:將驗證邏輯拆解或簡化,明確劃分「解碼」、「正規化」與「比較」三個步驟,提升程式碼的可讀性與可維護性。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:Base64 的驗證過程充滿了複雜的字串正規化與取代操作,讀起來像是在解迷宮,而非驗證身分。 **建議**:將驗證邏輯拆解或簡化,明確劃分「解碼」、「正規化」與「比較」三個步驟,提升程式碼的可讀性與可維護性。
if (available <= 0) {
this.truncated = true;
return;
}
const storedChunk = chunk.length > available ? chunk.subarray(0, available) : chunk;
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 validateAuth 中,僅透過 JSON.parse 檢查 JSON 格式,但未針對 Codex 預期的 auth.json 結構(如必要的欄位)進行 Schema 驗證。如果傳入的 JSON 格式正確但內容無效,可能會導致 codex exec 在後續執行時失敗。
建議:建議加入對 JSON 內容的簡單結構驗證(例如確認是否有 token 或必要的連線設定欄位)。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `validateAuth` 中,僅透過 `JSON.parse` 檢查 JSON 格式,但未針對 Codex 預期的 auth.json 結構(如必要的欄位)進行 Schema 驗證。如果傳入的 JSON 格式正確但內容無效,可能會導致 `codex exec` 在後續執行時失敗。 **建議**:建議加入對 JSON 內容的簡單結構驗證(例如確認是否有 `token` 或必要的連線設定欄位)。
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 appendGithubOutput 函式中,當 output 內容極大時,此處會將整個 output 字串在記憶體中進行檢查(output.includes(delimiter))與多次複製。這可能導致在處理極端長度輸出時發生記憶體不足的問題。
建議:建議限制 delimiter 嘗試次數,或在檢查時避免讀取整個 output 字串,改用串流處理方式。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `appendGithubOutput` 函式中,當 `output` 內容極大時,此處會將整個 `output` 字串在記憶體中進行檢查(`output.includes(delimiter)`)與多次複製。這可能導致在處理極端長度輸出時發生記憶體不足的問題。 **建議**:建議限制 `delimiter` 嘗試次數,或在檢查時避免讀取整個 `output` 字串,改用串流處理方式。
Review

嚴重等級🟡 警告
審查員:Maya
問題:OutputCollector 的截斷機制未被測試。
建議:增加測試案例模擬輸出超過 DEFAULT_OUTPUT_LIMIT_BYTES,驗證截斷提示。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:OutputCollector 的截斷機制未被測試。 **建議**:增加測試案例模擬輸出超過 DEFAULT_OUTPUT_LIMIT_BYTES,驗證截斷提示。
this.chunks.push(storedChunk);
this.size += storedChunk.length;
if (storedChunk.length < chunk.length) {
this.truncated = true;
}
}
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Rogue
問題:在 runCodex 中,使用 new Promise 封裝 child_process.spawn 並手動監聽 data 事件來拼接輸出。在高輸出量的場景下,不斷字串拼接(output += chunk.toString())會導致大量記憶體配置與 garbage collection 壓力。
建議:如果預期輸出量大,建議將 stdout/stderr 直接寫入檔案流或使用 Buffer 陣列收集後最後合併,減少中間字串變更帶來的記憶體浪費。

**嚴重等級**:🔵 建議 **審查員**:Rogue **問題**:在 `runCodex` 中,使用 `new Promise` 封裝 `child_process.spawn` 並手動監聽 data 事件來拼接輸出。在高輸出量的場景下,不斷字串拼接(`output += chunk.toString()`)會導致大量記憶體配置與 garbage collection 壓力。 **建議**:如果預期輸出量大,建議將 stdout/stderr 直接寫入檔案流或使用 `Buffer` 陣列收集後最後合併,減少中間字串變更帶來的記憶體浪費。
toString() {
const truncationMessage = this.truncated ? "\n[Output truncated]\n" : "";
return `${Buffer.concat(this.chunks, this.size).toString()}${truncationMessage}`;
}
}
function cleanup() {
tempFiles.cleanup();
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Bard
問題:makeTempDir 內部使用了硬編碼的 '.codex-action-' 前綴。
建議:將前綴提取為常數或設定檔參數。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:makeTempDir 內部使用了硬編碼的 '.codex-action-' 前綴。 **建議**:將前綴提取為常數或設定檔參數。
}
function makeTempDir(dir) {
const tempDir = fs.mkdtempSync(path.join(dir, TEMP_DIR_PREFIX));
fs.chmodSync(tempDir, DIR_MODE_PRIVATE);
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Leo
問題:在 runCodex 中使用了 --dangerously-bypass-approvals-and-sandbox,這類高風險標記若缺乏適當的說明,未來的維護者可能不清楚其安全意義而誤用或引發風險。
建議:建議在 spawn 呼叫前加上明確的註解,詳細說明為何在此環境中必須繞過沙盒,以及相關的安全考量。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:在 `runCodex` 中使用了 `--dangerously-bypass-approvals-and-sandbox`,這類高風險標記若缺乏適當的說明,未來的維護者可能不清楚其安全意義而誤用或引發風險。 **建議**:建議在 `spawn` 呼叫前加上明確的註解,詳細說明為何在此環境中必須繞過沙盒,以及相關的安全考量。
tempFiles.trackDir(tempDir);
return tempDir;
}
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:在執行外部指令時沒有設定逾時(timeout),若 Codex CLI 發生無預期的掛起(hang),Action 將會永久卡住而不會自動終止。
建議:建議在 spawn 的選項中加入 timeout 機制,或是主動在啟動後設置一個計時器,當執行時間過長時強制終止子行程。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在執行外部指令時沒有設定逾時(timeout),若 Codex CLI 發生無預期的掛起(hang),Action 將會永久卡住而不會自動終止。 **建議**:建議在 `spawn` 的選項中加入 `timeout` 機制,或是主動在啟動後設置一個計時器,當執行時間過長時強制終止子行程。
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);
Ghost marked this conversation as resolved
Review

嚴重等級🔴 嚴重
審查員:Assassin
問題:在執行 codex 子行程時,使用了 --dangerously-bypass-approvals-and-sandbox 參數。這會完全繞過沙盒機制與審核流程,如果 prompt 內容受到攻擊者控制,該 CLI 工具將獲得在容器中執行任意代碼的權限。
建議:移除該標記。如果必須使用,請確保 prompt 來源完全可信,並將執行權限嚴格限制在最小範圍內。應考慮透過其他機制進行必要的操作,而非直接繞過安全保護。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:在執行 `codex` 子行程時,使用了 `--dangerously-bypass-approvals-and-sandbox` 參數。這會完全繞過沙盒機制與審核流程,如果 `prompt` 內容受到攻擊者控制,該 CLI 工具將獲得在容器中執行任意代碼的權限。 **建議**:移除該標記。如果必須使用,請確保 `prompt` 來源完全可信,並將執行權限嚴格限制在最小範圍內。應考慮透過其他機制進行必要的操作,而非直接繞過安全保護。
fs.closeSync(fd);
tempFiles.trackFile(filePath);
return filePath;
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Rogue
問題runCodex 函數中的 spawn 使用 { stdio: ["ignore", "pipe", "pipe"] },這會導致 node 程式在輸出流被填滿時阻塞等待,即便透過 stdout.on('data') 監聽,在高輸出的情境下仍可能因為緩衝區管理不當而浪費不必要的 CPU 週期。
建議:如果預期輸出量很大,建議改用 child.stdout.pipe(process.stdout) 直接導向,而非透過 node 的事件迴圈在兩者間搬運資料。

**嚴重等級**:🔵 建議 **審查員**:Rogue **問題**:`runCodex` 函數中的 `spawn` 使用 `{ stdio: ["ignore", "pipe", "pipe"] }`,這會導致 node 程式在輸出流被填滿時阻塞等待,即便透過 `stdout.on('data')` 監聽,在高輸出的情境下仍可能因為緩衝區管理不當而浪費不必要的 CPU 週期。 **建議**:如果預期輸出量很大,建議改用 `child.stdout.pipe(process.stdout)` 直接導向,而非透過 node 的事件迴圈在兩者間搬運資料。
}
function appendGithubOutput(status, output) {
const outputFile = process.env.GITHUB_OUTPUT;
if (!outputFile) {
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 runCodex 中使用 spawn 時,沒有設定 cwd。如果 codex 工具依賴於當前工作目錄(例如需要編輯當前專案),這在 CI 環境中可能存在風險,雖然目前 CI 通常會設定好目錄,但這是一個隱含的契約。
建議:建議明確設定 cwd/github/workspace 或 CI 定義的專案根目錄,確保 codex 運作在預期的上下文中。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `runCodex` 中使用 `spawn` 時,沒有設定 `cwd`。如果 `codex` 工具依賴於當前工作目錄(例如需要編輯當前專案),這在 CI 環境中可能存在風險,雖然目前 CI 通常會設定好目錄,但這是一個隱含的契約。 **建議**:建議明確設定 `cwd` 為 `/github/workspace` 或 CI 定義的專案根目錄,確保 `codex` 運作在預期的上下文中。
return;
}
let delimiter;
do {
delimiter = `${OUTPUT_DELIMITER_PREFIX}${crypto.randomBytes(12).toString("hex")}`;
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Bard
問題:validateAuth 函式中對於 Base64 的正規化與驗證邏輯混雜在一起,使用了大量的取代與判斷,讀起來節奏凌亂,缺乏優雅感。
建議:將驗證邏輯與基礎轉換邏輯抽離,建議提取一個輔助函式專門負責 Base64 格式檢查,使主要流程清晰明瞭。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:validateAuth 函式中對於 Base64 的正規化與驗證邏輯混雜在一起,使用了大量的取代與判斷,讀起來節奏凌亂,缺乏優雅感。 **建議**:將驗證邏輯與基礎轉換邏輯抽離,建議提取一個輔助函式專門負責 Base64 格式檢查,使主要流程清晰明瞭。
} while (output.includes(delimiter));
fs.appendFileSync(
outputFile,
`status=${status}\noutput<<${delimiter}\n${output}${output.endsWith("\n") ? "" : "\n"}${delimiter}\n`,
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:直接將所有輸出串接在 outputChunks 中,若 CLI 輸出過大的日誌,可能會導致記憶體耗盡(OOM)。
建議:建議針對 output 大小設定上限,超過限制時截斷輸出,或是改用串流寫入暫存檔以避免將所有內容存於記憶體。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:直接將所有輸出串接在 `outputChunks` 中,若 CLI 輸出過大的日誌,可能會導致記憶體耗盡(OOM)。 **建議**:建議針對 output 大小設定上限,超過限制時截斷輸出,或是改用串流寫入暫存檔以避免將所有內容存於記憶體。
Review

嚴重等級🔴 嚴重
審查員:Mage
問題:這裡直接使用 spawn 執行 codex 命令,且參數 prompt 是直接從 process.env.PROMPT 讀取並傳入的。如果 CI 環境的 PROMPT 被惡意竄改,雖使用陣列傳遞參數避免了 shell injection,但 codex exec 的邏輯若沒有妥善限制(例如限制可執行指令類型),可能導致攻擊者在 CI Runner 環境執行任意指令。
建議:在 runCodex 函式中,除了已經加入的 --dangerously-bypass-approvals-and-sandbox 外,必須確保對 prompt 進行強力的白名單過濾,或改為使用非 exec 的子指令來限制權限。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:這裡直接使用 `spawn` 執行 `codex` 命令,且參數 `prompt` 是直接從 `process.env.PROMPT` 讀取並傳入的。如果 CI 環境的 `PROMPT` 被惡意竄改,雖使用陣列傳遞參數避免了 shell injection,但 `codex exec` 的邏輯若沒有妥善限制(例如限制可執行指令類型),可能導致攻擊者在 CI Runner 環境執行任意指令。 **建議**:在 `runCodex` 函式中,除了已經加入的 `--dangerously-bypass-approvals-and-sandbox` 外,必須確保對 `prompt` 進行強力的白名單過濾,或改為使用非 `exec` 的子指令來限制權限。
{ encoding: "utf8", mode: FILE_MODE_PRIVATE },
);
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Rogue
問題:在處理輸出區塊時,使用 chunks.reduce 重複計算陣列大小,隨著資料量增加,這會造成不必要的 O(n²) 運算瓶頸,浪費 CPU 週期。
建議:應在 closure 中維護一個 currentSize 變數來追蹤當前總大小,避免每次有新資料時都重新遍歷整個區塊陣列。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在處理輸出區塊時,使用 `chunks.reduce` 重複計算陣列大小,隨著資料量增加,這會造成不必要的 O(n²) 運算瓶頸,浪費 CPU 週期。 **建議**:應在 closure 中維護一個 `currentSize` 變數來追蹤當前總大小,避免每次有新資料時都重新遍歷整個區塊陣列。
Review

嚴重等級🟡 警告
審查員:Rogue
問題:處理輸出區塊時使用 chunks.reduce 重複計算陣列大小,造成 O(n²) 運算瓶頸。
建議:在 closure 中維護 currentSize 變數追蹤總大小,避免遍歷。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:處理輸出區塊時使用 chunks.reduce 重複計算陣列大小,造成 O(n²) 運算瓶頸。 **建議**:在 closure 中維護 currentSize 變數追蹤總大小,避免遍歷。
}
function fail(message, code = 1) {
console.error(message);
appendGithubOutput("failed", message);
cleanup();
process.exit(code);
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Maya
問題:測試檔案 tests/entrypoint_test.sh 有測試 missing_codex_command,這很好。但實作中對於 codex 執行失敗的各種細節(如權限不足、找不到 binary 等)都統一處理為 status: 1 和簡單的訊息,測試僅驗證了 failure 狀態,未驗證具體錯誤來源。
建議:考慮在 main.js 中根據不同的錯誤類型回傳更細緻的 status code,並在測試中驗證這些 code,能更精確地協助 CI 使用者除錯。

**嚴重等級**:🔵 建議 **審查員**:Maya **問題**:測試檔案 `tests/entrypoint_test.sh` 有測試 `missing_codex_command`,這很好。但實作中對於 `codex` 執行失敗的各種細節(如權限不足、找不到 binary 等)都統一處理為 `status: 1` 和簡單的訊息,測試僅驗證了 failure 狀態,未驗證具體錯誤來源。 **建議**:考慮在 `main.js` 中根據不同的錯誤類型回傳更細緻的 status code,並在測試中驗證這些 code,能更精確地協助 CI 使用者除錯。
Review

嚴重等級🔵 建議
審查員:Maya
問題:對於 codex 執行失敗的各種細節(權限不足、找不到 binary 等)都統一處理為 status: 1,測試未驗證具體錯誤來源。
建議:根據錯誤類型回傳細緻 status code,並在測試中驗證這些 code。

**嚴重等級**:🔵 建議 **審查員**:Maya **問題**:對於 codex 執行失敗的各種細節(權限不足、找不到 binary 等)都統一處理為 status: 1,測試未驗證具體錯誤來源。 **建議**:根據錯誤類型回傳細緻 status code,並在測試中驗證這些 code。
}
function compactBase64(value) {
return value.replace(/\s+/g, "");
}
function isBase64(value) {
const normalized = compactBase64(value);
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Rogue
問題:validateAuth 中重複執行 decoded.toString('base64') 並進行 normalizedBase64 處理,極度浪費資源。
建議:移除這些無謂的重新編碼比較,直接嘗試解碼。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:validateAuth 中重複執行 decoded.toString('base64') 並進行 normalizedBase64 處理,極度浪費資源。 **建議**:移除這些無謂的重新編碼比較,直接嘗試解碼。
const paddingIndex = normalized.indexOf("=");
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Assassin
問題:將 OAUTH 環境變數內容解碼並直接寫入 auth.json。雖然有檢查 base64 格式與 JSON 結構,但若解碼後的 JSON 內容包含惡意配置(如惡意插件路徑或偽造的 API 憑證),可能導致後續 codex CLI 在執行時被劫持或洩漏資料。
建議:除了驗證 JSON 結構外,應進一步驗證 auth.json 內的欄位是否符合預期格式,並限制其檔案權限為 600(已做),確保容器內其他行程無法讀取。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:將 `OAUTH` 環境變數內容解碼並直接寫入 `auth.json`。雖然有檢查 base64 格式與 JSON 結構,但若解碼後的 JSON 內容包含惡意配置(如惡意插件路徑或偽造的 API 憑證),可能導致後續 `codex` CLI 在執行時被劫持或洩漏資料。 **建議**:除了驗證 JSON 結構外,應進一步驗證 `auth.json` 內的欄位是否符合預期格式,並限制其檔案權限為 `600`(已做),確保容器內其他行程無法讀取。
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Leo
問題main 函式過於龐大且職責過多,它同時負責了訊號處理、路徑創建、檔案鎖定、認證驗證以及執行核心邏輯,這降低了程式碼的可讀性與單元測試的困難度。
建議:建議將 main 拆分為 validateInputsetupAuthrunCodexActioncleanup 等子函式,讓職責分離。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:`main` 函式過於龐大且職責過多,它同時負責了訊號處理、路徑創建、檔案鎖定、認證驗證以及執行核心邏輯,這降低了程式碼的可讀性與單元測試的困難度。 **建議**:建議將 `main` 拆分為 `validateInput`、`setupAuth`、`runCodexAction` 與 `cleanup` 等子函式,讓職責分離。
if (!normalized || normalized.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
return false;
}
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:在 runCodex 函數中雖然有處理 child.on('error', ...),但若 codex 指令本身不存在(spawn ENOENT),這裡捕捉到的 error stack trace 可能會包含完整的系統路徑資訊,這在 CI 環境中屬於資訊洩漏風險。
建議:建議在錯誤處理中,針對 error.code === 'ENOENT' 做明確判斷,回傳簡潔的錯誤訊息(例如「找不到 codex 指令」),而非直接回傳完整的 error.message

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在 `runCodex` 函數中雖然有處理 `child.on('error', ...)`,但若 `codex` 指令本身不存在(spawn ENOENT),這裡捕捉到的 error stack trace 可能會包含完整的系統路徑資訊,這在 CI 環境中屬於資訊洩漏風險。 **建議**:建議在錯誤處理中,針對 `error.code === 'ENOENT'` 做明確判斷,回傳簡潔的錯誤訊息(例如「找不到 codex 指令」),而非直接回傳完整的 `error.message`。
Review

嚴重等級🟡 警告
審查員:Maya
問題:spawn ENOENT 錯誤處理可能洩漏系統路徑資訊。
建議:針對 error.code === 'ENOENT' 做明確判斷,回傳簡潔錯誤訊息而非完整 stack trace。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:spawn ENOENT 錯誤處理可能洩漏系統路徑資訊。 **建議**:針對 error.code === 'ENOENT' 做明確判斷,回傳簡潔錯誤訊息而非完整 stack trace。
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 {
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:當 child.on('close', ...) 觸發時,若 code 為 null,預設回傳 1。雖然這處理了非預期終止,但缺少對 signal 終止(例如 SIGKILL)的具體紀錄,只知道失敗,無法區分是指令執行錯誤還是被系統殺掉。
建議:在 close 事件中,若 code 為 null,可以檢查 signal 參數(若有),並在 output 中加入被哪個 signal 終止的資訊,增加除錯便利性。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:當 `child.on('close', ...)` 觸發時,若 `code` 為 null,預設回傳 1。雖然這處理了非預期終止,但缺少對 signal 終止(例如 SIGKILL)的具體紀錄,只知道失敗,無法區分是指令執行錯誤還是被系統殺掉。 **建議**:在 `close` 事件中,若 `code` 為 null,可以檢查 `signal` 參數(若有),並在 output 中加入被哪個 signal 終止的資訊,增加除錯便利性。
Review

嚴重等級🟡 警告
審查員:Maya
問題:close 事件缺少對 signal 終止(如 SIGKILL)的具體紀錄。
建議:檢查 signal 參數,並在 output 中加入被哪個 signal 終止的資訊。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:close 事件缺少對 signal 終止(如 SIGKILL)的具體紀錄。 **建議**:檢查 signal 參數,並在 output 中加入被哪個 signal 終止的資訊。
fail("Decoded OAUTH must be a JSON object.");
}
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Bard
問題:runCodex 函式過於臃腫,包含了執行、超時處理、輸出截斷與錯誤捕捉等多重責任,這段旋律太過冗長且複雜。
建議:建議將輸出處理 (Output truncation logic) 與超時設定分離為獨立函式,以提升函式的可讀性與維護性。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:runCodex 函式過於臃腫,包含了執行、超時處理、輸出截斷與錯誤捕捉等多重責任,這段旋律太過冗長且複雜。 **建議**:建議將輸出處理 (Output truncation logic) 與超時設定分離為獨立函式,以提升函式的可讀性與維護性。
Review

嚴重等級🟡 警告
審查員:Rogue
問題:在 runCodex 的輸出處理中,每收到一塊資料就進行 Buffer.concattoString,若資料量大或封包碎,會產生大量不必要的記憶體配置與垃圾回收 (GC) 壓力。
建議:只在輸出完成、達到限制或必須輸出結果時才進行合併與轉型,不要在處理每一塊資料時都執行。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在 `runCodex` 的輸出處理中,每收到一塊資料就進行 `Buffer.concat` 與 `toString`,若資料量大或封包碎,會產生大量不必要的記憶體配置與垃圾回收 (GC) 壓力。 **建議**:只在輸出完成、達到限制或必須輸出結果時才進行合併與轉型,不要在處理每一塊資料時都執行。
Review

嚴重等級🟡 警告
審查員:Bard
問題:runCodex 函式過於臃腫,包含了過多職責。
建議:建議將輸出處理與超時設定分離為獨立函式。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:runCodex 函式過於臃腫,包含了過多職責。 **建議**:建議將輸出處理與超時設定分離為獨立函式。
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:parsePositiveInteger 的輸入回退機制未經測試。
建議:針對設定變數傳入無效數字或非法格式場景,驗證預設值套用。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:parsePositiveInteger 的輸入回退機制未經測試。 **建議**:針對設定變數傳入無效數字或非法格式場景,驗證預設值套用。
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;
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 child.on('close', ...) 事件中,使用 Buffer.concat(outputChunks).toString() 將所有輸出轉為單一字串。如果 outputChunks 總大小接近 DEFAULT_OUTPUT_LIMIT_BYTES (1MB),這會導致瞬間記憶體使用量增加,且對於極大輸出,字串轉換本身亦有潛在的負載。
建議:考慮使用 Buffer 處理後續輸出,或在達到 outputLimitBytes 時,僅保存 Buffer 片段即可,不必轉為大字串。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `child.on('close', ...)` 事件中,使用 `Buffer.concat(outputChunks).toString()` 將所有輸出轉為單一字串。如果 `outputChunks` 總大小接近 `DEFAULT_OUTPUT_LIMIT_BYTES` (1MB),這會導致瞬間記憶體使用量增加,且對於極大輸出,字串轉換本身亦有潛在的負載。 **建議**:考慮使用 `Buffer` 處理後續輸出,或在達到 `outputLimitBytes` 時,僅保存 `Buffer` 片段即可,不必轉為大字串。
}
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(),
};
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Bard
問題:spawn 函式參數陣列過長且散亂,閱讀性較差。
建議:將參數拆分為數組變數並展開傳遞。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:spawn 函式參數陣列過長且散亂,閱讀性較差。 **建議**:將參數拆分為數組變數並展開傳遞。
}
function codexExecArgs(model, prompt) {
return [
"exec",
"--dangerously-bypass-approvals-and-sandbox",
Ghost marked this conversation as resolved
Review

嚴重等級🔴 嚴重
審查員:Mage
問題:在 setupAuth 中,lockPath 使用 os.tmpdir()。在共享環境中,如果 CODEX_HOME 字串相同,會導致所有 process 競爭同一個鎖檔,且如果其他無關的 process 也剛好在 os.tmpdir() 中建立相同名稱的檔案,會導致誤判或鎖定失敗。
建議:應在 CODEX_HOME 內部建立鎖檔,而非使用全域的 os.tmpdir(),或者包含更具唯一性的識別碼(如 PID 或更長的路徑雜湊)以確保鎖的隔離性。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 `setupAuth` 中,`lockPath` 使用 `os.tmpdir()`。在共享環境中,如果 `CODEX_HOME` 字串相同,會導致所有 process 競爭同一個鎖檔,且如果其他無關的 process 也剛好在 `os.tmpdir()` 中建立相同名稱的檔案,會導致誤判或鎖定失敗。 **建議**:應在 `CODEX_HOME` 內部建立鎖檔,而非使用全域的 `os.tmpdir()`,或者包含更具唯一性的識別碼(如 PID 或更長的路徑雜湊)以確保鎖的隔離性。
Review

嚴重等級🔴 嚴重
審查員:Maya
問題:Codex 超時處理路徑未經測試,無法確保 SIGTERM 能成功發送與訊息正確產出。
建議:增加測試案例模擬長期睡眠(如 sleep 10),驗證超時機制與輸出訊息。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:Codex 超時處理路徑未經測試,無法確保 SIGTERM 能成功發送與訊息正確產出。 **建議**:增加測試案例模擬長期睡眠(如 sleep 10),驗證超時機制與輸出訊息。
"--skip-git-repo-check",
"--model",
model,
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Rogue
問題:在 setupAuth 中,lockPath 檔名產生使用了 Buffer.from(codexHome).toString("hex")。如果 codexHome 非常長,這個檔名可能會超過作業系統的檔案名稱長度限制(通常為 255 bytes),導致鎖定失敗,進而阻斷整個流程。
建議:改用 crypto.createHash('sha256').update(codexHome).digest('hex') 來產生固定長度的雜湊值作為檔名的一部分,既安全又保證長度可控。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:在 setupAuth 中,`lockPath` 檔名產生使用了 `Buffer.from(codexHome).toString("hex")`。如果 `codexHome` 非常長,這個檔名可能會超過作業系統的檔案名稱長度限制(通常為 255 bytes),導致鎖定失敗,進而阻斷整個流程。 **建議**:改用 `crypto.createHash('sha256').update(codexHome).digest('hex')` 來產生固定長度的雜湊值作為檔名的一部分,既安全又保證長度可控。
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.
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 setupAuth 中,在 fs.copyFileSync(authFile, authPath) 後立即 removeIfCreated(authFile),但在這期間如果發生 process 中斷,auth.json 可能會以不安全的權限(預設)或不完整的狀態寫入。
建議:建議使用 fs.renameSync 或在完成寫入與權限設定後再進行清理,並確保寫入過程中發生異常時能正確刪除該部分寫入的檔案。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `setupAuth` 中,在 `fs.copyFileSync(authFile, authPath)` 後立即 `removeIfCreated(authFile)`,但在這期間如果發生 process 中斷,auth.json 可能會以不安全的權限(預設)或不完整的狀態寫入。 **建議**:建議使用 `fs.renameSync` 或在完成寫入與權限設定後再進行清理,並確保寫入過程中發生異常時能正確刪除該部分寫入的檔案。
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() {
Ghost marked this conversation as resolved
Review

嚴重等級🔴 嚴重
審查員:Mage
問題:在 setupAuth 中,使用了 fs.renameSync 來確保原子性。然而在某些檔案系統中,若 authFileauthPath 不在同一個分區,renameSync 可能會失敗。此外,如果 codexHome 已存在且權限配置錯誤,fs.mkdirSync 可能會被忽略但後續存取失敗。
建議:建議確保 authFilecodexHome 處於相同掛載點,並增加對 fs.mkdirSync 後權限檢查的驗證。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 `setupAuth` 中,使用了 `fs.renameSync` 來確保原子性。然而在某些檔案系統中,若 `authFile` 與 `authPath` 不在同一個分區,`renameSync` 可能會失敗。此外,如果 `codexHome` 已存在且權限配置錯誤,`fs.mkdirSync` 可能會被忽略但後續存取失敗。 **建議**:建議確保 `authFile` 與 `codexHome` 處於相同掛載點,並增加對 `fs.mkdirSync` 後權限檢查的驗證。
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.");
}
}
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Bard
問題:setupAuth 函式職責過於繁雜。
建議:將鎖定機制與驗證機制拆分為獨立輔助函式。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:setupAuth 函式職責過於繁雜。 **建議**:將鎖定機制與驗證機制拆分為獨立輔助函式。
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
View File
@@ -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"
+23
View File
@@ -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
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Maya
問題:目前的 Docker 冒煙測試僅驗證了 CLI 二進位檔是否存在,但尚未驗證其在容器內執行時是否能正常存取與寫入 CODEX_HOME 環境設定的目錄。
建議:建議在 docker_image_test.sh 中增加一個測試案例,執行 codex --version 之外的指令,驗證容器權限與目錄環境變數設定是否正確。

**嚴重等級**:🔵 建議 **審查員**:Maya **問題**:目前的 Docker 冒煙測試僅驗證了 CLI 二進位檔是否存在,但尚未驗證其在容器內執行時是否能正常存取與寫入 `CODEX_HOME` 環境設定的目錄。 **建議**:建議在 `docker_image_test.sh` 中增加一個測試案例,執行 `codex --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"
+304
View File
@@ -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
Ghost marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Bard
問題:在測試中建立 codex 指令時,Here-document 與命令混雜,可讀性較低。
建議:建議使用更整齊的縮排格式,或將其抽離為獨立的測試輔助檔案。

**嚴重等級**:🟡 警告 **審查員**:Bard **問題**:在測試中建立 `codex` 指令時,Here-document 與命令混雜,可讀性較低。 **建議**:建議使用更整齊的縮排格式,或將其抽離為獨立的測試輔助檔案。
#!/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
Ghost marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Maya
問題:測試案例對於輸入參數的邊界測試(例如 PROMPT 為空字串、極長字串)不足。
建議:增加針對 PROMPT 輸入為空字串或是包含特殊 Shell 跳脫字元的測試案例。

**嚴重等級**:🔵 建議 **審查員**:Maya **問題**:測試案例對於輸入參數的邊界測試(例如 `PROMPT` 為空字串、極長字串)不足。 **建議**:增加針對 `PROMPT` 輸入為空字串或是包含特殊 Shell 跳脫字元的測試案例。
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"