Merge pull request 'feat(codex): 新增 Codex CLI composite action' (#1) from ai-review-resolve/develop-20260629-115924 into develop
Reviewed-on: composite-actions/codex#1
This commit was merged in pull request #1.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`。
|
||||
+42
-19
@@ -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"
|
||||
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<<CODEX_OUTPUT'
|
||||
printf '%s\n' "$text"
|
||||
echo 'CODEX_OUTPUT'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user