-
Notifications
You must be signed in to change notification settings - Fork 0
217 lines (187 loc) · 7.59 KB
/
Copy pathci.yaml
File metadata and controls
217 lines (187 loc) · 7.59 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
name: CI
# This repo is an auto-generated port: `scripts/upstream` runs an LLM against
# .upstreamer/upstreamer.md and opens a PR. CI is therefore the only mechanical
# thing between a generated diff and main, so it gates on the things that
# actually break a port — cross-version behavior, type safety in tests as well as
# src, coverage that cannot silently decay, and an installable wheel.
#
# Required checks (branch protection):
# check (py3.10) · check (py3.11) · check (py3.13) · types · build · verify-port
# Deliberately NOT required: e2e — it exits 0 when the API key is absent (forks),
# so requiring it would be a green rubber stamp.
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
# Supersede stale runs on a PR branch. Pushes to main are never cancelled: that
# would leave gaps in the main-branch signal.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# pyproject declares requires-python = ">=3.10", but CI used to test 3.11
# only, so a 3.10- or 3.13-specific break could land unnoticed. asyncio
# primitives are the real hazard here: asyncio.Condition() binds the running
# loop eagerly on older versions and lazily on 3.13.
check:
name: check (py${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
# Do not let a 3.10-only failure mask a 3.13-only failure.
fail-fast: false
matrix:
# 3.10 is the floor because `openrouter` 1.x requires >=3.10 (the SDK
# dropped 3.9 at 1.0.0). Python 3.9 reached EOL in October 2025.
python-version: ["3.10", "3.11", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
# --frozen: fail if uv.lock is out of sync with pyproject.toml rather than
# silently resolving something different from what was reviewed.
- run: uv sync --frozen --all-extras
- name: Lint
run: uv run ruff check .
- name: Format
run: uv run ruff format --check .
# Deterministic tests only. tests/e2e needs OPENROUTER_API_KEY and skips
# cleanly without it.
- name: Tests
if: matrix.python-version != '3.11'
run: uv run pytest tests/unit -q
# Coverage on one leg only: three legs would triple runtime to produce the
# same single number.
#
# Ratchet floor. Coverage may go up, never down — raise this when it rises.
# Lowering it is allowed only with an explicit reason in the PR body, since
# a port run that adds source without tests shows up here first.
- name: Tests with coverage
if: matrix.python-version == '3.11'
run: >-
uv run pytest tests/unit -q
--cov --cov-report=term-missing --cov-report=xml
--cov-fail-under=83
types:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: uv sync --frozen --all-extras
- name: Lockfile is in sync with pyproject
run: uv lock --check
# tests/ included on purpose: CI used to check src only, so every fake
# client and payload builder in tests/ was unverified — exactly where an
# Optional deref makes an assertion silently no-op. Not matrixed because
# [tool.mypy] python_version = "3.10" pins the analysis target, so the
# output is identical on every interpreter.
- name: Type check
run: uv run mypy src tests
# A package that imports fine from the source tree can still ship a broken
# wheel (missing package data, py.typed, or a module the build excludes).
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
# Built on the oldest supported interpreter so a wheel that only imports
# on newer syntax fails here rather than for a user on 3.10.
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Build wheel and sdist
run: uv build --out-dir dist
# --isolated --no-project: install only the built wheel, with the source
# tree off sys.path, so this proves the artifact rather than the repo.
- name: Import the public API from the built wheel
run: |
set -euo pipefail
wheel=$(ls dist/*.whl)
uv run --isolated --no-project --with "$wheel" python -c "
from openrouter_agent import call_model, OpenRouter, tool, ModelResult
import importlib.metadata as md
print('imported openrouter-agent-sdk', md.version('openrouter-agent-sdk'))"
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
# Live end-to-end tests against the real OpenRouter API: streaming, a real
# tool round, approval pause/resume, lifecycle hooks, state serialization
# round-trip. Costs a few cents per run (small model, short prompts).
#
# Warns and exits 0 when the secret is missing (e.g. PRs from forks, where
# GitHub withholds secrets) instead of failing — same pattern as upstream
# typescript-agent's e2e job. That is also why it must not be a required check.
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- run: uv sync --frozen --all-extras
- name: Live e2e tests
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: |
if [ -z "$OPENROUTER_API_KEY" ]; then
echo "::warning::OPENROUTER_API_KEY is not set; skipping live e2e tests."
exit 0
fi
uv run pytest tests/e2e -q
# The port's own mechanical gate — the same script `scripts/upstream` runs to
# decide whether .upstreamer/state.yaml may advance.
#
# Blocking. It was previously advisory on the premise that the required-API
# check "fails by design until the first sync lands"; that is no longer true —
# the verifier passes with all 31 required symbols exported and 0 failures, so
# advisory would only let that regress silently.
#
# It intentionally re-runs ruff/mypy/pytest that `check` also runs: the point is
# to exercise them exactly as the sync pipeline does, so CI and the port gate
# cannot drift apart.
verify-port:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/port-toolchain
- name: Port verifier
id: verify
run: |
set -o pipefail
./.upstreamer/scripts/verify.sh 2>&1 | tee /tmp/verify.log
- name: Summarize
if: always()
run: |
{
echo "## Port verifier"
echo
if [ "${{ steps.verify.outcome }}" = "success" ]; then
echo "Port is in sync with its parity floor."
else
echo "Parity floor broken — see the failures below."
fi
echo
echo '```'
cat /tmp/verify.log 2>/dev/null || echo "(no output)"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"