-
Notifications
You must be signed in to change notification settings - Fork 143
Expand file tree
/
Copy pathcode-review-model-benchmark.yml
More file actions
274 lines (258 loc) · 14.7 KB
/
Copy pathcode-review-model-benchmark.yml
File metadata and controls
274 lines (258 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
name: "Benchmark: tier-0 model code review"
# Per-model code-review benchmark against the QA cloud. For each recommended
# model in the BYOK catalog (curated-models.json, tier="recommended") it points
# ONE QA benchmark tenant's BYOK at the model and opens the fixed 5-PR set on the
# shared kodus-e2e benchmark repos, waiting for Kody to review each.
#
# Gate (mechanical): `benchmark:models` exits non-zero if any review fails to
# complete — that is the pass/fail. The precision/recall scorecard
# (`benchmark:scorecard`, judged by Sonnet vs golden_comments) is uploaded as an
# ARTIFACT for regression tracking against main — it is NOT a gate.
#
# Triggers (locked spec 2026-05-28, revised 2026-05-28):
# - push to main on engine paths → FULL run (5 models, 5 PRs each = 25
# reviews, ~$17, ~40min). Notify-only.
# - workflow_dispatch → manual: pin one model (only_model).
# NO pull_request trigger: the benchmark drives reviews on qa.web.kodus.io,
# which runs main's image — a PR-time LIGHT run would benchmark MAIN's
# engine quality (not the PR's), defeating the purpose. The right signal
# at PR-time is the kody rule "engine sem benchmark" reminding the dev to
# update the dataset; the FULL run after merge measures the actual change.
# NO nightly cron: full FULL run only when the engine actually changes,
# to keep cost bounded. Opus is dropped at the driver level
# (tests/e2e/benchmark/run.ts DEFAULT_EXCLUDED) — ~$23/run extra and not
# worth the CI spend. Run ad-hoc via dispatch with only_model="opus-4-7"
# if you ever need it.
#
# Required secrets (add in repo settings — only GH_TEST_TOKEN exists today):
# GH_TEST_TOKEN PAT that can see + write the kodus-e2e/* bench repos
# BYOK_ANTHROPIC_API_KEY Claude (sonnet-4-6, opus-4-7)
# BYOK_OPENAI_API_KEY gpt-5.4
# BYOK_GOOGLE_API_KEY gemini-3.1-pro
# BYOK_MOONSHOT_API_KEY kimi-k2.6 (openai_compatible)
# BYOK_ZHIPU_API_KEY glm-5.1 (openai_compatible)
# ANTHROPIC_API_KEY the scorecard judge (Sonnet)
# Optional: BENCH_TENANT_EMAIL / BENCH_TENANT_PASSWORD for a dedicated tenant.
permissions:
contents: read
on:
workflow_dispatch:
inputs:
only_model:
description: "Restrict to one model slug (e.g. sonnet-4-6, or opus-4-7 to run the otherwise-excluded model)"
required: false
default: ""
type: string
push:
branches: [main]
paths:
# Engine paths — anything that can change review OUTPUT quality.
# Stages/agents/prompts/rules/sandbox/BYOK + the model catalog.
- "libs/code-review/**"
- "libs/ee/codeBase/**"
- "libs/ee/codeReview/**"
- "libs/ee/kodyRules/**"
- "libs/kodyRules/**"
- "libs/sandbox/**"
- "libs/core/infrastructure/services/tokenTracking/**"
- "apps/web/src/features/ee/byok/_data/curated-models.json"
- "tests/e2e/benchmark/**"
- "scripts/benchmark/tier0-bench-prs.json"
concurrency:
# Single benchmark tenant + shared repos → never run two at once.
# cancel-in-progress: rapid successive engine merges collapse into one
# full run instead of queueing $17×N jobs. The latest commit's result is
# what matters; intermediate runs would just spend money.
group: code-review-model-benchmark
cancel-in-progress: true
jobs:
benchmark:
name: tier-0 model benchmark
runs-on: ubuntu-latest
# QA environment: home of QA_WAF_BYPASS_HEADER (the WAF Allow-rule
# header — runner IPs intermittently land on the QA WAF's block
# rules and every login 403s, the exact symptom described below).
# All triggers run from main, which the QA branch policy allows.
environment: QA
# 6 models x 5 PRs, each review polled up to 25 min. Full runs are long;
# GitHub's job ceiling is 6h. PR (light) runs are ~1 model.
timeout-minutes: 350
steps:
- name: Checkout monorepo
uses: actions/checkout@v6.0.2
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install e2e deps
working-directory: tests/e2e
run: npm install --no-audit --no-fund
- name: Decide scope (push main = full, dispatch = manual override)
id: scope
run: |
set -euo pipefail
ONLY_MODEL_INPUT="${{ github.event.inputs.only_model }}"
if [ "${{ github.event_name }}" = "push" ]; then
# FULL on merge: all recommended models EXCEPT the ones
# excluded at the driver level (Opus, see run.ts
# DEFAULT_EXCLUDED) — 5 models, ~$17.
echo "only_model=" >> "$GITHUB_OUTPUT"
echo "Full run (engine merged to main): 5 models (Opus excluded by driver)"
else
# workflow_dispatch — pass through whatever was given,
# including "opus-4-7" to opt into the otherwise-excluded model.
echo "only_model=$ONLY_MODEL_INPUT" >> "$GITHUB_OUTPUT"
echo "Manual dispatch: only_model='$ONLY_MODEL_INPUT'"
fi
- name: Wait for QA backend to settle on this build (avoid deploy-race)
env:
CLOUD_WEB_BASE_URL: ${{ vars.CLOUD_QA_WEB_URL || 'https://qa.web.kodus.io' }}
QA_WAF_BYPASS_HEADER: ${{ secrets.QA_WAF_BYPASS_HEADER }}
# The benchmark fires on the SAME push that triggers the QA GitOps
# deploy of github.sha. Gate on the live /health version matching
# this SHA so the benchmark BLOCKS until its own rollout has fully
# landed — and stays settled — before opening 25 PRs. Empty on
# workflow_dispatch (no deploy in flight) → pure readiness probe.
EXPECTED_VERSION: ${{ github.event_name == 'push' && github.sha || '' }}
run: |
set -uo pipefail
BASE="${CLOUD_WEB_BASE_URL%/}/api/proxy/api"
WAF_HDR=()
if [ -n "${QA_WAF_BYPASS_HEADER:-}" ]; then
WAF_HDR=(-H "x-kodus-e2e: ${QA_WAF_BYPASS_HEADER}")
fi
# On push this runs alongside the QA GitOps deploy, so it can hit
# the backend mid-rollout: the gateway answers 403/5xx/000, OR the
# API answers app-level for a moment and then restarts again as the
# rollout rolls across replicas — and a single momentary 200 is
# exactly how the review WORKER ends up not-yet-consuming when the
# 25 reviews fire (every model × repo then "review timeout", the
# 2026-06-12 red). So we do NOT proceed on the first healthy probe:
# we require the backend to stay app-ready AND on THIS build for
# several CONSECUTIVE checks, proving the rollout has settled.
echo "Probing $BASE/auth/login for app-level readiness (settle gate)…"
NEED_OK=3 # consecutive clean probes required to proceed
SETTLE_SLEEP=10 # spacing between probes (s)
ok_streak=0
for i in $(seq 1 90); do
code=$(curl -sS -o /dev/null -w '%{http_code}' \
-X POST "$BASE/auth/login" \
"${WAF_HDR[@]}" \
-H 'Content-Type: application/json' \
-d '{"email":"readiness-probe@invalid.kodus","password":"x"}' \
2>/dev/null || echo 000)
# If we know the target build, the live version must ALSO still
# match on each probe — a flip back to the prior build means a
# replica is mid-restart, so the rollout hasn't settled. Stays
# best-effort: an empty/'unknown' version can't be gated on, so
# it never blocks here.
ver_ok=1
if [ -n "${EXPECTED_VERSION:-}" ]; then
ver=$(curl -sS "${WAF_HDR[@]}" "$BASE/health" 2>/dev/null \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{console.log(JSON.parse(s).version||'')}catch{console.log('')}})" \
2>/dev/null || echo "")
case "$ver" in
""|unknown) ver_ok=1 ;;
*"$EXPECTED_VERSION"*|*"${EXPECTED_VERSION:0:7}"*) ver_ok=1 ;;
*) ver_ok=0 ;;
esac
fi
case "$code" in
200|201|400|401|404|422)
if [ "$ver_ok" = 1 ]; then
ok_streak=$((ok_streak + 1))
echo "attempt $i/90: app-ready (HTTP $code), settle streak $ok_streak/$NEED_OK"
if [ "$ok_streak" -ge "$NEED_OK" ]; then
echo "QA settled on this build — proceeding with the benchmark."
exit 0
fi
else
echo "attempt $i/90: app-ready (HTTP $code) but version='$ver' (rollout still moving) — resetting streak"
ok_streak=0
fi
sleep "$SETTLE_SLEEP" ;;
*)
echo "attempt $i/90: HTTP $code (gateway/rollout) — resetting streak, waiting ${SETTLE_SLEEP}s…"
ok_streak=0
sleep "$SETTLE_SLEEP" ;;
esac
done
echo "::error::QA backend never settled (app-ready + this build, $NEED_OK consecutive) in ~15min — aborting before the benchmark to avoid deploy-race false failures."
exit 1
- name: Run benchmark (mechanical gate — fails if any review fails)
working-directory: tests/e2e
env:
CLOUD_WEB_BASE_URL: ${{ vars.CLOUD_QA_WEB_URL || 'https://qa.web.kodus.io' }}
BENCH_ONLY_MODEL: ${{ steps.scope.outputs.only_model }}
BENCH_TENANT_EMAIL: ${{ secrets.BENCH_TENANT_EMAIL }}
BENCH_TENANT_PASSWORD: ${{ secrets.BENCH_TENANT_PASSWORD }}
# WAF Allow-rule header — lib/http.ts injects it on every
# qa.*.kodus.io request (see e2e-cloud.yml for rationale).
QA_WAF_BYPASS_HEADER: ${{ secrets.QA_WAF_BYPASS_HEADER }}
GH_TEST_TOKEN: ${{ secrets.GH_TEST_TOKEN }}
BYOK_ANTHROPIC_API_KEY: ${{ secrets.BYOK_ANTHROPIC_API_KEY }}
BYOK_OPENAI_API_KEY: ${{ secrets.BYOK_OPENAI_API_KEY }}
BYOK_GOOGLE_API_KEY: ${{ secrets.BYOK_GOOGLE_API_KEY }}
BYOK_MOONSHOT_API_KEY: ${{ secrets.BYOK_MOONSHOT_API_KEY }}
BYOK_ZHIPU_API_KEY: ${{ secrets.BYOK_ZHIPU_API_KEY }}
run: npm run benchmark:models
- name: Re-collect settled findings (repair late-posted findings)
if: always()
working-directory: tests/e2e
env:
GH_TEST_TOKEN: ${{ secrets.GH_TEST_TOKEN }}
run: npm run benchmark:recollect || echo "recollect step failed (non-gating)"
- name: Scorecard (artifact, not a gate)
id: scorecard
if: always()
working-directory: tests/e2e
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Never fail the job on the quality scorecard — the gate is the
# mechanical "all reviews completed" step above. Capture the printed
# table into a step output so the success notification can post it.
run: |
npm run benchmark:scorecard 2>&1 | tee /tmp/scorecard.txt || echo "scorecard step failed (non-gating)"
{
echo 'table<<SCORECARD_EOF'
grep -vE '^> |^npm |^$' /tmp/scorecard.txt | tail -n 40 || echo '(scorecard unavailable)'
echo 'SCORECARD_EOF'
} >> "$GITHUB_OUTPUT"
- name: Upload results + scorecard
if: always()
uses: actions/upload-artifact@v4
with:
name: tier0-benchmark-${{ github.run_id }}
path: |
tests/e2e/benchmark/results.json
tests/e2e/benchmark/scorecard.json
if-no-files-found: warn
retention-days: 30
- name: Notify Discord on failure
if: failure()
uses: ./.github/actions/discord-notify
with:
# Bench has its own audience (model quality watchers, not
# infra). Falls back to general so missing channel doesn't
# silence regressions.
webhook: ${{ secrets.DISCORD_WEBHOOK_INTERNAL || secrets.DISCORD_WEBHOOK }}
status: failure
title: "❌ Model code-review benchmark failed"
description: |
Engine code merged to main but the per-model benchmark didn't complete.
Scope: ${{ steps.scope.outputs.only_model != '' && format('only_model={0}', steps.scope.outputs.only_model) || 'FULL (5 models)' }}
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Trigger: ${{ github.event_name }}
- name: Notify Discord on success (scorecard)
if: success()
uses: ./.github/actions/discord-notify
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_INTERNAL || secrets.DISCORD_WEBHOOK }}
status: success
title: "✅ Model code-review benchmark — scorecard"
description: |
Scope: ${{ steps.scope.outputs.only_model != '' && format('only_model={0}', steps.scope.outputs.only_model) || 'FULL (5 models)' }}
```
${{ steps.scorecard.outputs.table }}
```
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}