Agent Arena

CI integration

Gating a merge on evaluation results.

Gate a merge on evaluation results.

A dedicated GitHub Action is planned, not shipped. What follows works today with a plain workflow step.

The gate

--fail-under exits non-zero when the winner's composite is below a threshold:

arena evaluate --project projects/my_project --fail-under 0.75 --quiet

That is the whole mechanism. Everything else is plumbing.

A workflow

name: Evaluate

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'pipelines/**'
      - 'projects/my_project/**'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: pip

      - run: pip install 'agent-arena[anthropic]'

      - name: Validate first
        run: arena validate --project projects/my_project

      - name: Evaluate
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          arena evaluate --project projects/my_project \
            --fail-under 0.75 --quiet

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: evaluation
          path: projects/my_project/results/

paths: matters. Evaluations cost money; running one on a README change is waste.

Keep the offline check separate

Split the free check from the paid one, and put the free one first:

      - name: Offline sanity — scorers and weights behave
        run: arena evaluate --project projects/my_project --models sim_small --quiet

      - name: Real models
        if: github.event_name == 'pull_request'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: arena evaluate --project projects/my_project --fail-under 0.75 --quiet

A broken scorer should fail in seconds against a mock model, not after spending on a real one.

Cost control in CI

Technique Effect
--limit 20 Cap the cases on a PR; run the full set nightly
--tags smoke A curated fast subset
--trials 1 Skip variance measurement on a PR
--dry-run on draft PRs Show the plan, spend nothing
paths: filters Do not run at all when nothing relevant changed
arena evaluate --project projects/my_project --tags smoke --trials 1 --limit 20 \
  --fail-under 0.75 --quiet

Machine-readable output

arena evaluate --project projects/my_project --json > result.json

Then post a comment, feed a dashboard, or assert on specific values:

python -c "
import json, sys
r = json.load(open('result.json'))
w = r['leaderboard']['entries'][0]
print(f\"::notice::Winner {w['key']} at {w['composite']:.3f}\")
sys.exit(0 if w['composite'] >= 0.75 else 1)
"

Tracking regressions over time

Commit the SQLite database, or restore it from a cache, and the history commands work in CI:

      - uses: actions/cache@v4
        with:
          path: projects/my_project/results/arena.sqlite
          key: arena-db-${{ github.ref }}
arena history --project projects/my_project --limit 5

That turns a single-point check into a trend, which is what catches a provider silently changing a model underneath you.

Secrets

  • Use your CI provider's secret store; never a literal key in a workflow file.
  • Scope the key to the minimum the evaluation needs.
  • A missing key means models are skipped, not failed — so a misconfigured secret can produce a green build that evaluated only your mock models. Assert on the model list, or use arena validate, which fails on missing credentials.

That last point is the one that bites. arena validate before arena evaluate is what turns it from a silent no-op into a failure.

The published action

.github/actions/agent-arena-eval wraps the steps above into one action: install, evaluate, and comment the leaderboard on the pull request.

- uses: adityamhaske/agent-arena/.github/actions/agent-arena-eval@main
  with:
    project: projects/support_triage
    fail-under: "0.75"        # optional; omit to comment without gating
Input Default Does
project (required) Path to the project folder
fail-under (none) Fails the step if the winner's composite is below this
baseline (none) Path to an arena export --format json file — usually the base branch's result — to show a delta column instead of just the new numbers
install-extra (none) e.g. anthropic, for a project using a real provider
github-token ${{ github.token }} Needs pull-requests: write to post the comment
python-version 3.12
Output Is
composite The winner's composite, or empty if everything was disqualified
winner The winner's model key
result-path Path to the full JSON result, for a later step to inspect

The comment is updated in place on every push to the PR rather than growing a new one each time — it is identified by an HTML comment marker, the usual convention for a bot comment that repeats.

Comparing against the base branch

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Get the base branch's result
        run: |
          git fetch origin ${{ github.base_ref }} --depth=1
          git show origin/${{ github.base_ref }}:projects/my_project/results/baseline.json             > baseline.json || echo '{"rankings":[]}' > baseline.json

      - uses: adityamhaske/agent-arena/.github/actions/agent-arena-eval@main
        with:
          project: projects/my_project
          baseline: baseline.json

That assumes a baseline.json is committed and refreshed on main — for example via arena export --format json --out projects/my_project/results/baseline.json in a workflow that runs on push to main. Without one, the action still posts the leaderboard; it just has no delta column.

The demo in this repo

.github/workflows/pr-eval-demo.yml dogfoods the action against projects/support_triage on every PR that touches it — offline, with mock: models, so it costs nothing and needs no secret. It is the action's own proof that it actually works in real GitHub Actions, on top of the unit tests in tests/test_pr_comment_action.py.

Never a hard gate the first time

Land the action without fail-under first, so it comments for a few PRs before anything can block a merge. A ranking is only worth gating on once someone has actually looked at what it says.