ci(port): track upstream HEAD instead of the latest published release… #28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |