diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 15c4240..95aa463 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,19 +1,34 @@ name: CI + on: pull_request: branches-ignore: - master types: [opened, synchronize] + jobs: - ai-code-review: - name: AI Code Review + release-tag-version: + name: Release Tag Version runs-on: ubuntu - permissions: - contents: write - pull-requests: write - issues: write + outputs: + version: ${{ steps.release-tag-version.outputs.version }} steps: - - name: AI 程式碼審查 by OpenCode - uses: https://gitea.jsc.idv.tw/composite-actions/opencode-code-review@${{ vars.ACTION_OPENCODE_CODE_REVIEW_VERSION }} + - name: 計算版本號 + id: release-tag-version + uses: https://gitea.jsc.idv.tw/composite-actions/release-tag-version@${{ vars.ACTION_RELEASE_TAG_VERSION }} with: - token: ${{ secrets.TOKEN }} + is_beta: 'true' + + codex: + name: Codex + runs-on: ubuntu + needs: release-tag-version + steps: + - name: 測試工具 + id: codex + uses: https://gitea.jsc.idv.tw/composite-actions/codex@v${{ needs.release-tag-version.outputs.version }} + with: + oauth: ${{ secrets.CODEX_OAUTH }} + - name: 檢查輸出 + if: ${{ steps.codex.outputs.text != vars.CODEX_EMAIL }} + run: exit 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f59026 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Codex CLI Composite Action + +此 repository 提供一個 Gitea/GitHub Actions composite action,用於安裝 Codex CLI、寫入 OAuth token、執行指定提示詞,並將 CLI 輸出寫入 action output。 + +## 專案列表 + +### 專案描述 + +| 專案名稱 | 專案描述 | +| --- | --- | +| [codex](https://gitea.jsc.idv.tw/composite-actions/codex) | 提供 Codex CLI composite action,讓 workflow 可透過 `oauth` 與 `prompt` 輸入執行 CLI,並從 repository variable 讀取模型名稱後取得文字輸出。 | + +### 參考專案 + +| 專案名稱 | 參考專案列表 | +| --- | --- | +| [codex](https://gitea.jsc.idv.tw/composite-actions/codex) | 無 | + +### NuGet 套件 + +| 專案名稱 | NuGet 套件列表 | +| --- | --- | +| [codex](https://gitea.jsc.idv.tw/composite-actions/codex) | 無 | + +## 功能列表 + +此 repository 未包含可列入 README 的 public method、public constructor、public extension method 或 public operator。 + +## 使用範例 + +### 在 workflow 中呼叫 Codex CLI + +以下範例示範在 workflow step 中呼叫此 composite action,傳入 OAuth token 與提示詞,並在後續步驟讀取 `text` output。模型名稱由 action 直接讀取 `vars.CODEX_MODEL`。 + +```yaml +- name: 執行 Codex CLI + id: codex + uses: https://gitea.jsc.idv.tw/composite-actions/codex@v1 + with: + oauth: ${{ secrets.CODEX_OAUTH }} + prompt: "請自我介紹" + +- name: 使用輸出文字 + run: printf '%s\n' '${{ steps.codex.outputs.text }}' +``` + +前置條件: + +- `secrets.CODEX_OAUTH` 必須是 Codex OAuth token 檔案內容的 base64 字串。 +- `vars.CODEX_MODEL` 必須是 Codex CLI 可用的模型名稱。 +- runner 必須可透過 npm 安裝 `@openai/codex`。 + +預期結果: + +- action 會安裝 `codex` CLI。 +- action 會將 OAuth token 寫入 `$HOME/.codex/auth.json` 並設定檔案權限為 `600`。 +- action 會執行 `codex exec --model "$MODEL" "$PROMPT"`,並把 stdout 寫入 `steps.codex.outputs.text`。 diff --git a/action.yml b/action.yml index 646c3c6..ef029ee 100644 --- a/action.yml +++ b/action.yml @@ -1,33 +1,56 @@ -name: 'Composite Action Template' -description: 'Composite Action 範本' +name: 'Codex CLI' +description: 'Codex CLI 工具' author: 'Jeffery' inputs: - text: - description: '輸入的文字' + prompt: + description: '傳給 Codex CLI 的提示詞' required: false - default: 'Hello, World!' + oauth: + description: 'base64 編碼的 Codex OAuth token 檔案內容' + required: true outputs: text: description: '輸出的文字' - value: ${{ steps.change.outputs.text }} + value: ${{ steps.codex.outputs.text }} runs: using: 'composite' steps: - - name: 交換 - id: change + - name: 安裝工具 env: - GITEA_SERVER_URL: ${{ gitea.server_url }} - GITEA_REPOSITORY: ${{ gitea.repository }} - GITEA_TOKEN: ${{ gitea.token }} - TEXT: ${{ inputs.text }} + OAUTH: ${{ inputs.oauth }} run: | - echo "Gitea Server Url: $GITEA_SERVER_URL" + if [ -z "$OAUTH" ]; then + echo 'oauth input (secrets.CODEX_OAUTH) is required and must not be empty.' >&2 + exit 1 + fi - echo "Gitea Repository: $GITEA_REPOSITORY" + npm install -g @openai/codex - echo "Gitea Token: $GITEA_TOKEN" - - echo "Text: $TEXT" - - echo "text=$TEXT" >> "$GITHUB_OUTPUT" - shell: bash \ No newline at end of file + oauth_file="$HOME/.codex/auth.json" + install -d -m 700 "$(dirname "$oauth_file")" + printf '%s' "$OAUTH" | base64 -d > "$oauth_file" + chmod 600 "$oauth_file" + shell: bash + - name: 執行工具 + id: codex + env: + MODEL: ${{ vars.CODEX_MODEL }} + PROMPT: ${{ inputs.prompt }} + run: | + if [ -z "$MODEL" ]; then + echo 'CODEX_MODEL repository variable is required.' >&2 + exit 1 + fi + if [ -z "$PROMPT" ]; then + email="$(python3 "$GITHUB_ACTION_PATH/app/codex_account.py")" + echo "text=$email" >> "$GITHUB_OUTPUT" + exit 0 + fi + text="$(codex exec --skip-git-repo-check -s danger-full-access --model "$MODEL" "$PROMPT")" + printf '%s\n' "$text" + { + echo 'text<> "$GITHUB_OUTPUT" + shell: bash diff --git a/app/codex_account.py b/app/codex_account.py new file mode 100644 index 0000000..10f5648 --- /dev/null +++ b/app/codex_account.py @@ -0,0 +1,57 @@ +"""透過 codex app-server 的 JSON-RPC account/read 取得目前登入帳號的 email。 + +這是 TUI `/status` Account 欄位的程式化來源,不解析本地 OAuth token, +而是由 codex 自身回報登入帳號。將 email 印到 stdout(取不到時印空字串)。 +""" + +import json +import subprocess +import time + + +def read_account_email(timeout_seconds: float = 25.0) -> str: + process = subprocess.Popen( + ["codex", "app-server"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + bufsize=1, + ) + + def send(obj): + process.stdin.write(json.dumps(obj) + "\n") + process.stdin.flush() + + send({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": {"clientInfo": {"name": "ci", "version": "1.0"}}, + }) + send({"jsonrpc": "2.0", "method": "initialized", "params": {}}) + send({"jsonrpc": "2.0", "id": 2, "method": "account/read", "params": {}}) + + email = "" + deadline = time.time() + timeout_seconds + try: + while time.time() < deadline: + line = process.stdout.readline() + if not line: + break + try: + message = json.loads(line) + except ValueError: + continue + if message.get("id") == 2: + account = (message.get("result") or {}).get("account") or {} + email = account.get("email") or "" + break + finally: + process.terminate() + + return email + + +if __name__ == "__main__": + print(read_account_email())