Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions .github/workflows/install-matrix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
name: Install Matrix + API Smoke

on:
pull_request:
push:
branches:
- main
workflow_dispatch:
inputs:
run_live_claude:
description: Run optional live Claude + Haiku validation
required: false
type: boolean
default: false
live_model:
description: Model ID for optional live validation
required: false
type: string
default: claude-haiku-4-5-20251001

permissions:
contents: read

jobs:
install-check:
name: Install ${{ matrix.install_mode }} | ${{ matrix.os }} | py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
python-version:
- '3.11'
- '3.12'
install_mode:
- pep517
- fallback
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ matrix.python-version }}

- name: Upgrade packaging tools
run: python -m pip install --upgrade pip setuptools wheel

- name: Install package with PEP 517 editable mode
if: ${{ matrix.install_mode == 'pep517' }}
run: python -m pip install -e . --use-pep517

- name: Install package with editable fallback mode
if: ${{ matrix.install_mode == 'fallback' }}
run: python -m pip install -e .

- name: Validate dependency graph
run: python -m pip check

- name: Validate runtime imports
run: python -c "import claude_code_api, greenlet, sqlalchemy; print('install-import-smoke-ok')"

api-smoke:
name: API smoke | ${{ matrix.os }} | py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
needs: install-check
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-latest
python-version:
- '3.11'
- '3.12'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ matrix.python-version }}

- name: Upgrade packaging tools
run: python -m pip install --upgrade pip setuptools wheel

- name: Install project with test extras
run: python -m pip install -e ".[test]" --use-pep517

- name: Run deterministic tests (excluding live e2e)
run: python -m pytest -q -m "not e2e"

- name: Run API health smoke test
run: python -m pytest -q tests/test_end_to_end.py::TestHealthAndBasics::test_health_check

- name: Run API models smoke test
run: python -m pytest -q tests/test_end_to_end.py::TestModelsAPI::test_list_models

- name: Upload test artifacts on failure
if: ${{ failure() }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: api-smoke-artifacts-${{ matrix.os }}-py${{ matrix.python-version }}
path: |
.pytest_cache
dist/tests
if-no-files-found: ignore
retention-days: 14

live-claude-haiku:
name: Live Claude + Haiku validation (manual)
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_live_claude == 'true' }}
runs-on: ubuntu-latest
needs: api-smoke
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: '3.12'

- name: Upgrade packaging tools
run: python -m pip install --upgrade pip setuptools wheel

- name: Install project with test extras
run: python -m pip install -e ".[test]" --use-pep517

- name: Install Claude CLI
run: curl -fsSL https://claude.ai/install.sh | bash
Comment on lines +137 to +138
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Piping curl directly to bash introduces a supply-chain risk.

Using curl -fsSL … | bash makes the workflow dependent on executing whatever that endpoint serves at runtime. Where possible, prefer a more controlled install path (e.g., versioned release or package manager) and/or pin via checksum or signature verification so a compromised install endpoint can’t silently impact CI runs.

Suggested implementation:

      - name: Install Claude CLI
        env:
          CLAUDE_INSTALL_URL: https://claude.ai/install.sh
          # TODO: Replace this placeholder with the official SHA256 for the installer at CLAUDE_INSTALL_URL
          CLAUDE_INSTALL_SHA256: "<expected-sha256-of-install.sh>"
        run: |
          set -euo pipefail
          curl -fsSL "$CLAUDE_INSTALL_URL" -o /tmp/claude-install.sh

          echo "$CLAUDE_INSTALL_SHA256  /tmp/claude-install.sh" | sha256sum -c -

          bash /tmp/claude-install.sh

  1. Determine the official SHA256 checksum for the https://claude.ai/install.sh script (e.g., from release notes or by downloading the script once in a controlled environment and computing sha256sum install.sh).
  2. Replace the "<expected-sha256-of-install.sh>" placeholder with that exact checksum value so the workflow will fail if the installer content changes unexpectedly.
  3. Optionally, you may want to pin CLAUDE_INSTALL_URL to a specific versioned installer URL if the project provides one, further reducing the risk of unexpected changes.


- name: Add Claude CLI path
run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Verify ANTHROPIC_API_KEY is configured
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
echo "ANTHROPIC_API_KEY secret is required for live Claude validation."
exit 1
fi

- name: Configure Claude auth from API key
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
python - <<'PY'
import json
import os
from pathlib import Path

config_dir = Path.home() / ".config" / "claude"
config_dir.mkdir(parents=True, exist_ok=True)
config = {"apiKey": os.environ["ANTHROPIC_API_KEY"], "autoUpdate": False}
with (config_dir / "config.json").open("w", encoding="utf-8") as handle:
json.dump(config, handle)
PY

- name: Start API server
run: |
nohup python -m claude_code_api.main > api-server.log 2>&1 &
echo "$!" > api-server.pid

- name: Wait for health endpoint
run: |
python - <<'PY'
import time
import urllib.error
import urllib.request

url = "http://127.0.0.1:8000/health"
deadline = time.time() + 60
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=5) as response:
if response.status == 200:
raise SystemExit(0)
except urllib.error.URLError:
pass
time.sleep(2)

raise SystemExit("API health check failed after 60 seconds")
PY

- name: Run live E2E against Haiku
env:
CLAUDE_CODE_API_E2E: '1'
CLAUDE_CODE_API_BASE_URL: http://127.0.0.1:8000
CLAUDE_CODE_API_TEST_MODEL: ${{ github.event.inputs.live_model || 'claude-haiku-4-5-20251001' }}
run: python -m pytest -q tests/test_e2e_live_api.py -m e2e -v

- name: Stop API server
if: ${{ always() }}
run: |
if [ -f api-server.pid ]; then
kill "$(cat api-server.pid)" || true
fi

- name: Upload live validation logs
if: ${{ always() }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: live-claude-haiku-logs
path: |
api-server.log
dist/tests
if-no-files-found: ignore
retention-days: 14
3 changes: 1 addition & 2 deletions claude_code_api/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
update,
)
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import declarative_base, relationship

from claude_code_api.models.claude import get_default_model
from claude_code_api.utils.time import utc_now
Expand Down
6 changes: 1 addition & 5 deletions claude_code_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,7 @@ async def http_exception_handler(request, exc):
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
"""Return OpenAI-style errors for validation failures."""
status_code = getattr(
status,
"HTTP_422_UNPROCESSABLE_CONTENT",
status.HTTP_422_UNPROCESSABLE_ENTITY,
)
status_code = getattr(status, "HTTP_422_UNPROCESSABLE_CONTENT", 422)
for error in exc.errors():
if error.get("type") in {"value_error.jsondecode", "json_invalid"}:
status_code = status.HTTP_400_BAD_REQUEST
Expand Down
17 changes: 17 additions & 0 deletions docs/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ Coverage-only artifacts for Sonar:
make coverage-sonar
```

## CI Matrix Testing

- Cross-platform install and API smoke checks run in `.github/workflows/install-matrix.yml`.
- Matrix coverage includes:
- OS: `ubuntu-latest`, `macos-latest`, `windows-latest`
- Python: `3.11`, `3.12`
- Install modes: explicit PEP 517 editable and editable fallback path (`pip install -e .`)
- The workflow runs deterministic tests with `pytest -m "not e2e"` and targeted API smoke checks for `/health` and `/v1/models`.

Optional live validation (manual only):

- Trigger `Install Matrix + API Smoke` via `workflow_dispatch`.
- Set `run_live_claude=true`.
- Provide repository secret `ANTHROPIC_API_KEY`.
- Optional input `live_model` defaults to `claude-haiku-4-5-20251001`.
- The live job is non-blocking (`continue-on-error`) while reliability is being established.

## Logging

- Logging is configured centrally in `claude_code_api/core/logging_config.py`.
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
"python-multipart>=0.0.6",
"pydantic-settings>=2.1.0",
"sqlalchemy>=2.0.23",
"greenlet>=3.0.0",
"aiosqlite>=0.19.0",
"alembic>=1.13.0",
"passlib[bcrypt]>=1.7.4",
Expand All @@ -48,6 +49,7 @@ test = [
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.1.0",
"httpx>=0.25.0",
"requests>=2.31.0",
"pytest-mock>=3.12.0",
]
dev = [
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"python-multipart>=0.0.6",
"pydantic-settings>=2.1.0",
"sqlalchemy>=2.0.23",
"greenlet>=3.0.0",
"aiosqlite>=0.19.0",
"alembic>=1.13.0",
"passlib[bcrypt]>=1.7.4",
Expand All @@ -36,6 +37,7 @@
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.1.0",
"httpx>=0.25.0",
"requests>=2.31.0",
"pytest-mock>=3.12.0",
],
"dev": [
Expand Down
Loading