name: CI on: push: branches: [main, "feature/**"] pull_request: branches: [main] schedule: - cron: "0 3 * * *" # Nightly at 03:00 UTC — runs docker-e2e workflow_dispatch: jobs: test-shard: runs-on: ubuntu-latest strategy: fail-fast: false matrix: group: [1, 2, 3, 4] steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install uv uses: astral-sh/setup-uv@v7 - name: Install dependencies run: uv pip install --system ".[dev,server]" - name: Run tests (shard ${{ matrix.group }}/4) # pytest-split shards the suite across 4 parallel jobs, balanced by # the committed `.test_durations` file; `-n auto` parallelises # within each shard across the runner's cores. Regenerate durations # with `pytest tests/ --store-durations -n auto` when the suite # drifts enough that shards become uneven. run: pytest tests/ -v --tb=short -n auto --splits 4 --group ${{ matrix.group }} env: TESTING: "1" # Single required status check. Branch protection requires `test`, but the # matrix above publishes `test-shard (1..4)` — this job aggregates them # into one `test` result so no branch-protection change is needed. test: needs: test-shard if: always() runs-on: ubuntu-latest steps: - name: Verify all test shards passed run: | if [ "${{ needs.test-shard.result }}" != "success" ]; then echo "::error::test-shard result was '${{ needs.test-shard.result }}' — one or more shards failed" exit 1 fi echo "All 4 test shards passed." lint: # Advisory only (continue-on-error) — ruff + mypy surface issues but # never gate. Split out of release.yml's old test job; runs without the # full dependency install since neither tool needs it. runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Lint with ruff run: | pip install ruff ruff check . || true continue-on-error: true - name: Type check with mypy run: | pip install mypy mypy src/ app/ cli/ connectors/ --ignore-missing-imports --no-error-summary || true continue-on-error: true cli-wheel-clean-install: # Catches the "wheel METADATA conflicts with transitive deps under fresh # resolver" class — exactly what the workspace-only `[tool.uv] # override-dependencies` does NOT protect against. Builds the wheel the # way `release.yml` ships it to analysts (`uv build --wheel`), then # installs it into a fresh `python:3.13-slim` container with `uv tool # install` (the path the `/setup` page advertises) and asserts the # `agnes` binary actually launches. Without this, a regression like # 0.53.3's `kbcstorage>=0.9.0 → urllib3<2.0.0` cap silently caps the # wheel METADATA, every existing test passes (workspace overrides the # cap), and the break only surfaces on the next analyst's first install. runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install uv uses: astral-sh/setup-uv@v7 - name: Build wheel run: uv build --wheel --out-dir dist - name: Write CLI install assertion script run: | cat > /tmp/smoke.py <<'PY' import sys, urllib3 try: import kbcstorage # noqa: F401 sys.exit("REGRESSION: kbcstorage leaked into the CLI wheel — should be in [server] extra only") except ImportError: pass maj, minor = (int(x) for x in urllib3.__version__.split(".")[:2]) assert (maj, minor) >= (2, 7), f"urllib3 too old: {urllib3.__version__}" print(f"OK: kbcstorage absent, urllib3 {urllib3.__version__}") PY - name: Smoke install in fresh python:3.13-slim run: | docker run --rm \ -v "$PWD/dist:/wheels:ro" \ -v /tmp/smoke.py:/smoke.py:ro \ python:3.13-slim bash -c ' set -euo pipefail apt-get update -qq && apt-get install -y -qq --no-install-recommends curl ca-certificates >/dev/null curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1 export PATH="$HOME/.local/bin:$PATH" WHEEL=$(ls /wheels/agnes_the_ai_analyst-*-py3-none-any.whl | head -1) uv tool install --force "$WHEEL" agnes --version agnes --help > /dev/null agnes catalog --help > /dev/null # Run the assertion in the same venv uv tool created "$HOME/.local/share/uv/tools/agnes-the-ai-analyst/bin/python" /smoke.py ' docker-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Build Docker image run: docker build -t data-analyst:test . docker-e2e: runs-on: ubuntu-latest if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' needs: docker-build steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Install uv uses: astral-sh/setup-uv@v7 - name: Install dependencies run: uv pip install --system ".[dev,server]" - name: Start services run: | touch .env docker compose up -d --wait --wait-timeout 60 - name: Run Docker E2E tests run: pytest tests/ -v --tb=short -m docker --timeout=120 env: TESTING: "1" - name: Stop services if: always() run: docker compose down