diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json new file mode 100644 index 0000000..4c740a0 --- /dev/null +++ b/.gitea/ai-review/exclusions.json @@ -0,0 +1,44 @@ +[ + { + "location": "action.yml:94", + "role": "Assassin", + "original_finding": "儘量避免將敏感 Token 直接透過環境變數傳遞至 Shell 命令。若必須傳遞,請確保該 Runner 的環境配置已設定遮蔽機制,或使用 GitHub Actions 內建的 `secrets` 處理方式(如 `github.token` 或預先設定好的 Secret Context)來存取,而非直接由外部輸入。", + "reason": "Composite action 無法直接讀取呼叫端 secrets context,必須由 workflow 以 input/env 傳入;此處 token 只在 runner 內以 secret 值傳給 curl,且未輸出到 log,屬 CI secret 傳遞慣用模式。" + }, + { + "location": "action.yml:78", + "role": "Mage", + "original_finding": "建議在 `printf` 使用變數時,確保輸入內容已適當轉義,或者考慮使用將變數內容寫入檔案並透過 `cat` 讀取的方式,避免直接在 `printf` 字串中展開變數。", + "reason": "此處使用 printf 的固定格式字串與 %s 參數,且變數皆以雙引號包住;issue/comment 內容中的雙引號不會重新進入 shell 語法解析,因此不會造成 shell 注入或解析錯誤。" + }, + { + "location": "action.yml:85", + "role": "Leo", + "original_finding": "建議將與 Gitea API 互動的邏輯封裝成一個獨立的 shell script 或另一個獨立的 Action,使此 composite action 的 `runs` 階段更清晰,專注於流程編排而非細節實作。", + "reason": "此 action 的核心職責就是產生 AI 回覆並留言到 Gitea issue;目前 inline API 呼叫很短且已包含 URL 驗證、空回覆檢查與 JSON escaping,額外拆成 script/action 對此小型 composite action 並非必要修復。" + }, + { + "location": "action.yml:96", + "role": "Maya", + "original_finding": "應補上測試案例,驗證當 `GITEA_SERVER_URL` 輸入為非預期格式、空白或惡意字串時,系統是否能正確攔截並處理。", + "reason": "目前 action 已以白名單與空值檢查在執行期攔截非預期 Gitea URL;此 repository 沒有既有測試框架,為單一 composite action 額外建立 mock runner 測試超出本次修復範圍。" + }, + { + "location": "action.yml:109", + "role": "Maya", + "original_finding": "建議模擬 `curl` 接收到不同 HTTP 錯誤代碼的情境,驗證 Workflow 是否會正確執行失敗並留下相應錯誤訊息。", + "reason": "curl 已使用 --fail-with-body,非 2xx 會使 workflow 失敗並輸出 API body;此 repository 沒有既有測試框架可模擬 HTTP 狀態碼,新增完整 mock 測試不屬於目前 action 修復的最小範圍。" + }, + { + "location": "action.yml:85", + "role": "Maya", + "original_finding": "建議在測試案例中模擬 `steps.codex` 輸出為空字串,以驗證是否確實會以錯誤狀態結束 Workflow。", + "reason": "空回覆已在 action 內以 [ -z \"$RESPONSE\" ] 明確失敗;此 repository 沒有既有測試框架,無法在不新增大型 mock harness 的情況下驗證 steps.codex output。" + }, + { + "location": "action.yml:80", + "role": "Rogue", + "original_finding": "直接使用 bash 的字串操作功能進行 URL 清理,對於 API 請求,應考慮將所有邏輯合併為單一腳本區塊,避免多次呼叫外部進程(如 `jq`)。建議可以直接利用 curl 的 --json 選項(若版本支援)或更直接的處理方式減少對外部依賴的 fork。", + "reason": "此處保留 jq 是為了正確處理多行與特殊字元的 JSON escaping,避免手寫 JSON 或依賴 runner curl 版本的 --json 支援;安全與相容性優先於移除單一外部程序。" + } +] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json new file mode 100644 index 0000000..8d6f7d0 --- /dev/null +++ b/.gitea/ai-review/findings.json @@ -0,0 +1,26 @@ +[ + { + "level": "warning", + "role": "Bard", + "location": "action.yml:113", + "problem": "在腳本中直接硬編碼了 Gitea server URL `https://gitea.jsc.idv.tw`,這限制了行動模組的可移植性,若未來更改 Server 位址需動態修改多處程式碼。", + "suggestion": "建議將允許的 Gitea Server URL 設為行動模組的 input 參數,並將此限制提取至組態層級,避免直接硬編碼於執行邏輯內。", + "is_new": true + }, + { + "level": "warning", + "role": "Bard", + "location": "action.yml:95", + "problem": "在 `產生 AI 回覆` 的 `with` 區塊中,參數名為 `oauth`,與 `inputs` 定義的 `codex_auth_config` 命名不一致,造成閱讀上的語義斷層。", + "suggestion": "建議將 `with` 區塊中的 `oauth` 重新命名為 `codex_auth_config`,以確保命名與 input 定義的一致性。", + "is_new": true + }, + { + "level": "info", + "role": "Bard", + "location": "action.yml:54", + "problem": "在 `cat < prompt.md - - opencode run --agent plan "$(cat prompt.md)" > response.md - - - name: 留言到問題 - env: - GITEA_REPOSITORY: ${{ gitea.repository }} - GITEA_SERVER_URL: ${{ gitea.server_url }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - RUNNER_TOKEN: ${{ secrets.RUNNER_TOKEN }} - run: | - jq -n --rawfile body response.md '{body: $body}' > comment.json - - curl --fail-with-body \ - -X POST \ - -H "Authorization: token $RUNNER_TOKEN" \ - -H "Content-Type: application/json" \ - --data @comment.json \ - "$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/issues/$ISSUE_NUMBER/comments" + - name: 取得專案 + uses: actions/checkout@v6 + - name: 產生並留言 AI 回覆 + uses: https://gitea.jsc.idv.tw/actions/codex-response@v${{ needs.version.outputs.version }} + with: + gitea_server_url: ${{ gitea.server_url }} + gitea_repository: ${{ gitea.repository }} + gitea_token: ${{ secrets.RUNNER_TOKEN }} + codex_auth_config: ${{ secrets.CODEX_OAUTH }} + model: gpt-5.4-mini + issue_title: ${{ github.event.issue.title }} + issue_body: ${{ github.event.issue.body }} + issue_number: ${{ github.event.issue.number }} + comment_body: ${{ github.event.comment.body }} diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml new file mode 100644 index 0000000..30e1dcb --- /dev/null +++ b/.gitea/workflows/review.yaml @@ -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.RUNNER_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 diff --git a/action.yml b/action.yml index 028357c..680b89e 100644 --- a/action.yml +++ b/action.yml @@ -1,31 +1,134 @@ -name: 'Composite Action Template' -description: 'Composite Action 範本' +name: 'Codex Issue Response' +description: '使用 Codex 產生 Gitea issue 留言回覆' author: 'Jeffery' inputs: - runner_token: - description: 'Gitea Runner Token' + gitea_server_url: + description: 'Gitea server URL' + required: true + gitea_repository: + description: 'Gitea repository, for example owner/repo' + required: true + gitea_token: + description: 'Gitea Personal Access Token for API authorization' + required: true + codex_auth_config: + description: 'Base64 encoded Codex auth.json' + required: true + model: + description: 'Codex model name' + default: 'gpt-5.4-mini' + issue_title: + description: 'Issue title' + required: true + issue_body: + description: 'Issue body' + required: false + default: '' + issue_number: + description: 'Issue number' + required: true + comment_body: + description: 'Issue comment body' required: true - text: - description: '輸入的文字' - default: "Hello, World!" outputs: - text: - description: '輸出的文字' - value: ${{ steps.display.outputs.text }} + status: + description: 'Codex execution result status' + value: ${{ steps.codex.outputs.status }} + response: + description: 'Codex execution result response' + value: ${{ steps.codex.outputs.output }} runs: using: 'composite' steps: - - name: 顯示資訊 - id: display + - name: 建立 Prompt + id: prompt env: - GITEA_SERVER_URL: ${{ gitea.server_url }} - GITEA_REPOSITORY: ${{ gitea.repository }} - RUNNER_TOKEN: ${{ inputs.runner_token }} - TEXT: ${{ inputs.text }} + ISSUE_TITLE: ${{ inputs.issue_title }} + ISSUE_BODY: ${{ inputs.issue_body }} + COMMENT_BODY: ${{ inputs.comment_body }} run: | - echo "Gitea Server Url: $GITEA_SERVER_URL" - echo "Gitea Repository: $GITEA_REPOSITORY" - echo "Gitea Runner Token: $RUNNER_TOKEN" - echo "Input Text: $TEXT" - echo "text=$TEXT" >> "$GITHUB_OUTPUT" - shell: bash \ No newline at end of file + shopt -s extglob + clean_comment="${COMMENT_BODY//@codex/}" + clean_comment="${clean_comment##+([[:space:]])}" + clean_comment="${clean_comment%%+([[:space:]])}" + + if [ -z "$ISSUE_TITLE" ]; then + echo 'Issue title is required' >&2 + exit 1 + fi + + if [ -z "$ISSUE_BODY" ]; then + ISSUE_BODY='(無描述)' + fi + + if [ -z "$clean_comment" ]; then + clean_comment='(未提供額外留言)' + fi + + { + echo 'prompt<<__PROMPT__' + cat <> "$GITHUB_OUTPUT" + shell: bash + - name: 產生 AI 回覆 + id: codex + uses: https://gitea.jsc.idv.tw/actions/codex@v0.0.2 + with: + oauth: ${{ inputs.codex_auth_config }} + model: ${{ inputs.model }} + prompt: ${{ steps.prompt.outputs.prompt }} + - name: 留言 AI 回覆 + env: + GITEA_REPOSITORY: ${{ inputs.gitea_repository }} + GITEA_SERVER_URL: ${{ inputs.gitea_server_url }} + GITEA_TOKEN: ${{ inputs.gitea_token }} + ISSUE_NUMBER: ${{ inputs.issue_number }} + RESPONSE: ${{ steps.codex.outputs.output }} + run: | + gitea_server_url="${GITEA_SERVER_URL%/}" + + if [ -z "$gitea_server_url" ]; then + echo 'Gitea server URL is required' >&2 + exit 1 + fi + + case "$gitea_server_url" in + https://gitea.jsc.idv.tw) ;; + *) + echo "Unsupported Gitea server URL: $gitea_server_url" >&2 + exit 1 + ;; + esac + + if [ -z "$RESPONSE" ]; then + echo 'Codex response is empty' >&2 + exit 1 + fi + + comment_payload="$(printf '%s' "$RESPONSE" | jq -Rs '{body: .}')" + + curl --fail-with-body \ + -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + --data "$comment_payload" \ + "$gitea_server_url/api/v1/repos/$GITEA_REPOSITORY/issues/$ISSUE_NUMBER/comments" + shell: bash