Day 11 把完整行為案例做成 JSONL eval,runner 會驗證 status、normalized_host、evidence 與 next_step,並區分產品不符與設定損壞。今天把 pytest 和 eval runner 接進 CI,讓每次修改腳本、來源 registry 或 SKILL.md 都必須通過同一套門檻。
一個可靠的 CI job 還要回答:
只寫一行 pytest 不代表規格已經成為合併門檻。
用 requirements-dev.txt 明確鎖住測試與 schema 驗證依賴:
pytest==8.3.5
PyYAML==6.0.2
jsonschema==4.23.0
若專案已有 lockfile,就使用同一套管理工具,不要同時維護兩份真相。重點是 CI 不應在每次執行時悄悄抓到新的 major 或不相容版本。
.github/workflows/verify-ticket-guard.yml:
name: verify-ticket-guard
on:
pull_request:
paths:
- "SKILL.md"
- "scripts/**"
- "references/**"
- "tests/**"
- "evals/**"
- "requirements-dev.txt"
- ".github/workflows/verify-ticket-guard.yml"
push:
branches: [main]
paths:
- "SKILL.md"
- "scripts/**"
- "references/**"
- "tests/**"
- "evals/**"
- "requirements-dev.txt"
- ".github/workflows/verify-ticket-guard.yml"
路徑要涵蓋規格、實作、來源、測試、eval 與 workflow 自身。只監控 scripts/** 會讓人改了 sources.md 或 expected cases 卻不跑驗證。
permissions:
contents: read
concurrency:
group: verify-ticket-guard-${{ github.ref }}
cancel-in-progress: true
驗證工作只需要讀 repository。相同 branch 推新 commit 時取消舊 run,避免過期結果占用 runner,也避免 reviewer 看錯版本。
jobs:
verify:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: requirements-dev.txt
- name: Install dependencies
run: python -m pip install --require-hashes -r requirements-dev.txt
ubuntu-24.04 與 Python 3.12 明確指定,不依賴 latest。若要使用 --require-hashes,requirements 必須真的包含每個 wheel/source 的 hash;否則先移除這個 flag,不能貼上一個看似安全但必定失敗的指令。
第一版也可以用:
- name: Install dependencies
run: python -m pip install -r requirements-dev.txt
然後在下一次維護加入可重現 hash。文章中的安全選項必須和 repository 現況一致。
- name: Run unit tests
run: python -m pytest -q
- name: Run behavior evals
run: |
mkdir -p artifacts
python evals/run_evals.py \
--cases evals/cases.jsonl \
--schema evals/schema.json \
--sources references/sources.md \
--json-output artifacts/eval-summary.json
分開的 step 讓失敗位置一眼可見。不要用:
python -m pytest -q && python evals/run_evals.py ...
那會把兩層契約混成一段 log,也讓第二個命令因第一個失敗而完全沒有狀態。
Day 11 已定義:
CI 不需要改寫這些 code。GitHub Actions 會把非零視為失敗,而 log 中保留 1/2 的診斷語意。不要寫 || true,也不要用 continue-on-error: true 把紅燈改成綠燈。
若 runner 在 exit 1 前寫出 JSON summary,可以用 if: always() 保存 artifact:
- name: Upload eval summary
if: always() && hashFiles('artifacts/eval-summary.json') != ''
uses: actions/upload-artifact@v4
with:
name: eval-summary
path: artifacts/eval-summary.json
if-no-files-found: error
retention-days: 14
Artifact 不應包含來源頁面內容、憑證、環境變數或完整 traceback。只保留 case id、不一致欄位和通過/失敗計數。
Exit 2 可能發生在 runner 尚未建立 summary 之前。hashFiles(...) != '' 讓 upload step 在檔案不存在時跳過,而不是用第二個錯誤蓋掉原始原因。
若想讓所有失敗都有 artifact,runner 可以先建立最小 envelope,再開始載入:
{
"status": "configuration_error",
"message": "cases.jsonl:4: invalid JSON",
"total": 0,
"passed": 0,
"failed": 0
}
訊息要可診斷,但不能把整份來源 registry 複製進 artifact。
name: verify-ticket-guard
on:
pull_request:
paths:
- "SKILL.md"
- "scripts/**"
- "references/**"
- "tests/**"
- "evals/**"
- "requirements-dev.txt"
- ".github/workflows/verify-ticket-guard.yml"
push:
branches: [main]
paths:
- "SKILL.md"
- "scripts/**"
- "references/**"
- "tests/**"
- "evals/**"
- "requirements-dev.txt"
- ".github/workflows/verify-ticket-guard.yml"
permissions:
contents: read
concurrency:
group: verify-ticket-guard-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: requirements-dev.txt
- name: Install dependencies
run: python -m pip install -r requirements-dev.txt
- name: Run unit tests
run: python -m pytest -q
- name: Run behavior evals
run: |
mkdir -p artifacts
python evals/run_evals.py \
--cases evals/cases.jsonl \
--schema evals/schema.json \
--sources references/sources.md \
--json-output artifacts/eval-summary.json
- name: Upload eval summary
if: always() && hashFiles('artifacts/eval-summary.json') != ''
uses: actions/upload-artifact@v4
with:
name: eval-summary
path: artifacts/eval-summary.json
if-no-files-found: error
retention-days: 14
這份 workflow 不需要 repository secret,也沒有寫入權限。若驗證流程要求 token 才能讀測試資料,表示 fixture 或架構邊界需要重新檢查。
域名分類的 eval 應使用本地來源 fixture,不應在 CI 中抓 organizer 網站或 DNS:
來源證據的定期更新是另一個 workflow;核心回歸測試必須離線、確定、快速。
actions/checkout@v4、setup-python@v5、upload-artifact@v4 比追 main 穩定,但 tag 仍可能移動。高敏感 repository 可以 pin 到完整 commit SHA,再用 Dependabot 更新:
dependabot:
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
不要手寫一個假的 SHA。真正 pin 時,從 action 官方 repository 的 release/commit 取得並 code review。
Workflow 成功不代表合併一定被阻擋。Repository 設定還要把 verify-ticket-guard / verify 設為 required status check,並要求 branch 是最新版本後才能合併。
確認步驟:
Required check 名稱是 API 與設定的一部分。不要頻繁改 job id,否則 branch protection 可能指向不存在的 check。
如果 PR 只改 README,workflow 不會執行。這是刻意的節省,但 required check 必須配合 GitHub 的 path-filter 行為測試。若平台讓被跳過的 required workflow 永遠 pending,改用永遠觸發 workflow,再在 job 內決定是否執行重測。
最安全的第一版其實是所有 PR 都跑。這組測試應該快速,省下幾十秒不值得換一個合併死鎖。
[ ] 本機 pytest 通過
[ ] 本機 eval 通過
[ ] PR 會觸發 verify job
[ ] 產品 mismatch 顯示可讀 diff,exit 1
[ ] 設定損壞顯示行號/欄位,exit 2
[ ] eval summary 在失敗時仍可取得
[ ] workflow 只有 contents: read
[ ] required check 真正阻擋紅燈 PR
[ ] 核心 eval 不依賴外部網路
CI 的價值不是「有一個綠色勾勾」,而是把可驗證規格變成每次合併都無法跳過的工程約束。
Day 13 回到 SKILL.md,設計分流與失敗政策:哪些輸入直接拒絕、哪些標成 UNCONFIRMED、哪些情況可以繼續到後續流程。