chore(deps)(deps): update mcp requirement from <2.0,>=1.0 to >=1.0,<3.0 #686
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
| # GitHub Actions CI for SchemaBrain. | |
| # | |
| # Two jobs run on every push to `main` and every pull request: | |
| # - lint-and-unit: ruff + pytest unit tests across Python 3.11 + 3.12. | |
| # Fast (target: under 60s warm cache); blocks merge. | |
| # - integration: pytest integration tests (Docker / testcontainers | |
| # Postgres). Single Python (3.11) since the integration | |
| # surface is dialect-driven, not interpreter-driven. | |
| # Slower (~2-3 min, dominated by container boot); also | |
| # blocks merge so we don't ship regressions in the | |
| # Postgres connector or profiler. | |
| # | |
| # Both jobs use `uv` for env management — same tool the dev workflow uses, | |
| # so CI failures reproduce locally with | |
| # `uv sync --extra dev --extra otel --extra ui && uv run pytest`. | |
| # | |
| # OS matrix: ubuntu-latest only at v0. macOS is documented as supported | |
| # for local dev on Python 3.11, but `onnxruntime` (a fastembed transitive | |
| # dep) has no wheel for macOS arm64 + Python 3.12 today, so adding macOS | |
| # to CI would either drop a Python version or fail the macOS+3.12 cell. | |
| # Revisit when onnxruntime ships the missing wheel. | |
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| # Manual escape hatch: re-run CI against any branch from the | |
| # Actions UI or `gh workflow run CI --ref <branch>`. Useful when a | |
| # transient platform issue (stuck queue, partial outage) leaves | |
| # a PR without a fresh status check and the auto-trigger fired | |
| # while the workflow was in a degraded state. | |
| workflow_dispatch: | |
| # Cancel in-flight runs for the same PR / branch when a new push lands — | |
| # saves CI minutes on rapid iteration without losing any signal. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Default least-privilege token for all jobs. The workflow only reads | |
| # the checked-out source tree; nothing here pushes, comments, or | |
| # publishes. Individual jobs that need elevated permissions must | |
| # declare them explicitly. Hardens against a compromised CI step | |
| # (e.g. a malicious dep / semgrep rule pack) ever exfiltrating or | |
| # pushing to the repo. | |
| permissions: | |
| contents: read | |
| jobs: | |
| lint-and-unit: | |
| name: Lint + unit (Python ${{ matrix.python-version }}) | |
| runs-on: ubuntu-latest | |
| # Hard cap on runaway tests / fetches. The job typically finishes | |
| # in under 60 s with a warm cache; 10 min is a generous ceiling | |
| # that still bounds CI-minute burn on a hang. | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.11", "3.12"] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| # Pinned to an immutable point tag (NOT @v8 or @v8.0) because | |
| # astral-sh/setup-uv stopped publishing rolling major/minor tags | |
| # in v8.0.0 as supply-chain hardening (response to the tj-actions | |
| # incident; see | |
| # https://github.com/astral-sh/setup-uv/releases/tag/v8.0.0). | |
| # The previously-used `@v7` rolling tag was deleted upstream on | |
| # 2026-05-26, breaking every CI in the world pinned to it. The | |
| # 3 other usages of this action in this file + the 1 in | |
| # `publish.yml` are pinned to the same point tag; bump them | |
| # together. Dependabot manages bumps via the `github-actions` | |
| # ecosystem in `.github/dependabot.yml`. | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: "uv.lock" | |
| - name: Set up Python ${{ matrix.python-version }} | |
| run: uv python install ${{ matrix.python-version }} | |
| - name: Install dependencies | |
| # `--extra otel` enables the OpenTelemetry test paths | |
| # (`test_observability_otel.py` + `test_observability_instrument_otel.py`). | |
| # `--extra ui` enables the dashboard sidecar test paths | |
| # (`tests/dashboard/`). Both extras add ~5–6 MB each and are | |
| # required to keep the OTel + dashboard modules at full coverage | |
| # when the integration job runs the gate. | |
| run: uv sync --extra dev --extra otel --extra ui --python ${{ matrix.python-version }} | |
| - name: Lint (ruff check) | |
| run: uv run --python ${{ matrix.python-version }} ruff check schemabrain tests scripts | |
| - name: Format check (ruff format --check) | |
| run: uv run --python ${{ matrix.python-version }} ruff format --check schemabrain tests scripts | |
| - name: Charter lint (Levels 1 + 2) | |
| # PR-gate enforcement of the MCP charter (Principle 2 description | |
| # rules + Pydantic envelope round-trip). Runs before the unit | |
| # suite so a description drift fails fast on cheap signal. | |
| run: uv run --python ${{ matrix.python-version }} python scripts/charter_lint.py | |
| - name: Unit tests | |
| # No coverage gate here — unit-only runs miss code paths | |
| # exercised by integration tests (Postgres connector, profiler). | |
| # The 99% line+branch gate runs in the `integration` job, which | |
| # sees the full suite. This job is the fast PR-feedback signal. | |
| # | |
| # `not slow` deselects the pytest-benchmark perf gate (10k-column | |
| # retrieval p95). The benchmark's thresholds are tuned for | |
| # local development hardware; shared GitHub-hosted x86_64 | |
| # runners are 2-3x slower on SQLite I/O and produce false | |
| # positives. Run the perf gate locally with `pytest -m slow` | |
| # before merging perf-sensitive changes. | |
| # `not firewall_bypass` excludes the opt-in regression corpus at | |
| # tests/firewall/. Tests there assert SECURE behaviour and FAIL | |
| # on `main` until each fix lands; explicit invocation only. | |
| run: uv run --python ${{ matrix.python-version }} pytest -m "not integration and not slow and not firewall_bypass" | |
| security: | |
| name: Security scans (pip-audit + bandit + semgrep) | |
| runs-on: ubuntu-latest | |
| # Sequence after lint-and-unit. A typo / syntax error has no business | |
| # burning a ~90-second `uv sync` + 3 scanner runs only to be obviated | |
| # by a `ruff check` failure that lands first. The critical path of | |
| # the workflow is `lint-and-unit -> integration` (~3-4 min); the | |
| # security job finishes well inside that window even when sequenced, | |
| # so the parallel-for-speed argument doesn't apply here. | |
| needs: lint-and-unit | |
| # Strict ceiling so a hung semgrep registry fetch can't burn 6 hours | |
| # of CI minutes (GitHub's default). | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: "uv.lock" | |
| - name: Set up Python 3.11 | |
| run: uv python install 3.11 | |
| - name: Install dependencies | |
| # `--extra otel` + `--extra ui` keep the lockfile-derived install | |
| # identical across all jobs so a security scan that depends on | |
| # installed versions sees the same surface as lint-and-unit. | |
| run: uv sync --extra dev --extra otel --extra ui --python 3.11 | |
| - name: pip-audit (known-CVE deps) | |
| # Strict mode: any vulnerability fails the build. | |
| # | |
| # Audit a uv-exported requirements file rather than the | |
| # installed environment. The `--no-emit-project` flag | |
| # excludes the local schemabrain project from the export so | |
| # pip-audit only sees declared dependencies. This avoids the | |
| # chicken-and-egg where release branches that bump | |
| # `pyproject.toml` ahead of a PyPI publish fail because | |
| # pip-audit can't find the new version on PyPI to check CVEs | |
| # against. Auditing dependencies is what we actually care | |
| # about; the project's own CVEs are caught at release-time | |
| # smoke tests, not here. | |
| # | |
| # Documented suppressions (revisit at every dependency bump): | |
| # - PYSEC-2025-183 (pyjwt 2.12.1) — transitive dep of the | |
| # `mcp` SDK; no fix version published as of 2026-05-20. | |
| # We don't construct, sign, or verify JWTs anywhere in | |
| # schemabrain; the only path that loads pyjwt is the MCP | |
| # server's OAuth metadata endpoint, which we don't expose. | |
| # Drop this suppression when the MCP SDK ships a release | |
| # that pins a fixed pyjwt version. | |
| run: | | |
| uv export --no-emit-project --format requirements-txt --quiet > /tmp/audit-requirements.txt | |
| uv run --python 3.11 pip-audit --strict \ | |
| --requirement /tmp/audit-requirements.txt \ | |
| --ignore-vuln PYSEC-2025-183 | |
| - name: bandit (Python SAST) | |
| # Reads `[tool.bandit]` from pyproject.toml so the test/scripts | |
| # exclusions and any future suppressions live in one place. | |
| # `-ll` raises confidence + severity threshold to "low" — i.e., | |
| # we fail on Low/Medium/High of either, which is the strictest | |
| # mode bandit ships. Inline `# nosec B608` comments justify the | |
| # two known-safe identifier-only f-strings in | |
| # `profiler/postgres.py`; any other finding fails the build. | |
| run: | | |
| uv run --python 3.11 bandit \ | |
| -c pyproject.toml \ | |
| -r schemabrain/ \ | |
| -ll | |
| - name: semgrep (broader Python + security-audit ruleset) | |
| # `uvx` (not a direct dev dep) because semgrep ships a heavy | |
| # binary (~46 MB) AND would balloon `uv sync --extra dev` for | |
| # every contributor. The version is PINNED — without that, a | |
| # malicious or buggy semgrep release between runs would flip | |
| # the `--error` gate red on a benign codebase and block every | |
| # PR until we tracked down which floating dep changed. The | |
| # rule packs (`p/python` + `p/security-audit`) still fetch | |
| # from the Semgrep registry at run time, so rules update | |
| # independently of the binary version. Dependabot doesn't | |
| # currently track uvx-pinned tools — bump this manually when | |
| # a fresh CVE or false-positive forces it. | |
| run: | | |
| uvx semgrep@1.163.0 scan \ | |
| --config p/python \ | |
| --config p/security-audit \ | |
| --error \ | |
| schemabrain/ | |
| integration: | |
| name: Integration (Docker / Postgres) | |
| runs-on: ubuntu-latest | |
| # Run after lint-and-unit so a syntax error doesn't waste 3 min on | |
| # container boot. Either lint-and-unit cell green is enough — we don't | |
| # need both Python versions to pass before integration starts. | |
| needs: lint-and-unit | |
| # Generous because container boot adds ~30-60 s on top of the test | |
| # suite. A hang in testcontainers / Docker would otherwise burn | |
| # the GitHub-default 6-hour ceiling. | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: "uv.lock" | |
| - name: Set up Python 3.11 | |
| run: uv python install 3.11 | |
| - name: Install dependencies | |
| # `--extra otel` is REQUIRED here — without it the OTel test | |
| # paths (20 tests across two files) skip with | |
| # `schemabrain[otel] not installed`, dropping branch coverage | |
| # below the 99% gate. Same posture as lint-and-unit so the | |
| # tests that run locally with `uv sync --extra dev --extra otel | |
| # --extra ui` match the CI surface exactly. `--extra ui` is | |
| # similarly required so the `tests/dashboard/` paths run and | |
| # keep dashboard sidecar coverage. | |
| run: uv sync --extra dev --extra otel --extra ui --python 3.11 | |
| - name: Pre-pull Postgres image (warm testcontainers cache) | |
| run: docker pull postgres:16-alpine | |
| - name: Full suite with coverage gate | |
| # Runs BOTH unit and integration tests together so the coverage | |
| # report sees every path. The 99% gate is a one-percentage-point | |
| # buffer below the project's 100% line+branch target — flags | |
| # genuine regressions without thrashing on minor edge cases. | |
| run: | | |
| uv run --python 3.11 pytest \ | |
| --cov=schemabrain \ | |
| --cov-branch \ | |
| --cov-report=term-missing \ | |
| --cov-fail-under=99 | |
| docker-build-smoke: | |
| name: Docker build smoke (linux/amd64) | |
| # Only runs when the Dockerfile, .dockerignore, package metadata, or | |
| # source tree changes. Skipping it on docs-only PRs keeps the typical | |
| # PR critical path fast; running it when it matters catches Dockerfile | |
| # breakage before release time instead of at publish-workflow time. | |
| runs-on: ubuntu-latest | |
| needs: lint-and-unit | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Detect Docker-relevant changes | |
| id: filter | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| docker: | |
| - 'Dockerfile' | |
| - '.dockerignore' | |
| - 'pyproject.toml' | |
| - 'uv.lock' | |
| - 'schemabrain/**' | |
| - '.github/workflows/ci.yml' | |
| - name: Set up Buildx | |
| if: steps.filter.outputs.docker == 'true' | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build image (no push) | |
| if: steps.filter.outputs.docker == 'true' | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: linux/amd64 | |
| push: false | |
| load: true | |
| tags: schemabrain:smoke | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Smoke-test --help | |
| if: steps.filter.outputs.docker == 'true' | |
| run: | | |
| OUT=$(docker run --rm schemabrain:smoke --help) | |
| echo "$OUT" | |
| echo "$OUT" | grep -q "usage:" | |
| dashboard-e2e: | |
| name: Dashboard E2E (Playwright) | |
| # Only runs when something the smoke could actually catch changes: | |
| # the web app, the FastAPI sidecar package, the demo seed script | |
| # (which boots the stack), or this workflow file itself. Skipping | |
| # docs-only and core-Python-only PRs keeps the typical critical | |
| # path lean while still catching dashboard regressions whenever | |
| # they could have been introduced. | |
| runs-on: ubuntu-latest | |
| needs: lint-and-unit | |
| # Generous because the job boots a sidecar, installs Chromium, | |
| # and runs 5 Playwright specs against a real browser. Warm-cache | |
| # local runs land in ~30 s; CI cold-cache + browser install pushes | |
| # toward ~3-4 min. 12 min ceiling caps a hang without burning the | |
| # default 6-hour quota. | |
| timeout-minutes: 12 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Detect dashboard-relevant changes | |
| id: filter | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| dashboard: | |
| - 'web/**' | |
| - 'schemabrain/dashboard/**' | |
| - 'scripts/dashboard_demo.py' | |
| - '.github/workflows/ci.yml' | |
| - name: Install uv | |
| if: steps.filter.outputs.dashboard == 'true' | |
| # Same point-tag pin as the rest of this workflow. See the | |
| # lint-and-unit job for the full rationale on why @v8 is unsafe. | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: "uv.lock" | |
| - name: Set up Python 3.11 | |
| if: steps.filter.outputs.dashboard == 'true' | |
| run: uv python install 3.11 | |
| - name: Install Python dependencies (+ui extra) | |
| # `--extra ui` pulls in fastapi + uvicorn + sse-starlette so the | |
| # sidecar can boot. `--extra dev` covers everything `dashboard_demo.py` | |
| # imports from `schemabrain.*`. OTel is not on this path. | |
| if: steps.filter.outputs.dashboard == 'true' | |
| run: uv sync --extra dev --extra ui --python 3.11 | |
| - name: Set up pnpm | |
| if: steps.filter.outputs.dashboard == 'true' | |
| # Pinned to v6 (immutable major); pnpm version comes from | |
| # `web/package.json` engines.pnpm (>=9). Keep this in sync with | |
| # the engines field if it ever bumps. | |
| uses: pnpm/action-setup@v6 | |
| with: | |
| version: 9 | |
| - name: Set up Node 20 | |
| if: steps.filter.outputs.dashboard == 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "pnpm" | |
| cache-dependency-path: web/pnpm-lock.yaml | |
| - name: Install web dependencies | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| # `--frozen-lockfile` is the CI default for pnpm but stating | |
| # it explicitly fails loudly if anyone hand-edits package.json | |
| # without regenerating the lockfile. | |
| run: pnpm install --frozen-lockfile | |
| - name: Component unit tests + coverage gate (Vitest) | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| # jsdom component tests for the design-system kit + theme hook. | |
| # No sidecar or browser needed, so it runs ahead of the Chromium | |
| # install + sidecar boot to fail fast on a kit regression. Coverage | |
| # is scoped to components/kit + lib/useTheme (see vitest.config.ts); | |
| # the run fails if any metric drops below the configured threshold. | |
| run: pnpm vitest run --coverage | |
| - name: Cache Playwright browsers | |
| if: steps.filter.outputs.dashboard == 'true' | |
| id: playwright-cache | |
| uses: actions/cache@v5 | |
| with: | |
| # Browser binaries live under ~/.cache/ms-playwright; cache | |
| # key is bound to the Playwright version so a bump | |
| # invalidates and forces a fresh install with matching | |
| # protocol support. | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ hashFiles('web/pnpm-lock.yaml') }} | |
| - name: Install Playwright Chromium (+OS deps) | |
| # `--with-deps` installs the apt packages Chromium needs on | |
| # ubuntu-latest. Cheap to run unconditionally (no-op when the | |
| # browser cache hit) and safer than gating on cache-hit, since | |
| # the system deps live outside the cached browser dir. | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Build dashboard static export | |
| # `schemabrain/dashboard/static/` is gitignored — built at | |
| # release time from the Next.js app, not committed. Without | |
| # this step the sidecar serves only the landing fallback and | |
| # every spec fails with "element not found" against the | |
| # built-app text. Mirrors how the wheel is assembled for | |
| # `pip install schemabrain[ui]`. | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| run: pnpm run export | |
| - name: Web perf budget (bundle size + reactflow isolation + fonts swap) | |
| # Deterministic half of wsQA-perf-budget-landing for the dashboard app: | |
| # asserts the /overview first-load JS/CSS stays within budget, reactflow | |
| # stays dynamic-imported on /graph only (never an eager chunk), and every | |
| # fetched @font-face uses font-display: swap. Reads web/.next from the | |
| # export above. LCP/CLS stay a manual Lighthouse check (README). | |
| if: steps.filter.outputs.dashboard == 'true' | |
| run: node scripts/check_bundle_budget.mjs --app web --route '/(app)/overview/page' --js 200 --css 50 --forbid-module 'react-flow__' --fonts-swap | |
| - name: Boot dashboard sidecar (headless) | |
| if: steps.filter.outputs.dashboard == 'true' | |
| # Background the demo script so the subsequent steps can talk | |
| # to it. `--no-open` suppresses the browser launch (which would | |
| # have nothing to attach to on a CI runner anyway). | |
| run: | | |
| uv run --python 3.11 python scripts/dashboard_demo.py --no-open & | |
| echo "SIDECAR_PID=$!" >> "$GITHUB_ENV" | |
| - name: Wait for sidecar health | |
| if: steps.filter.outputs.dashboard == 'true' | |
| # 30s ceiling — sidecar boot is ~1-2 s warm; budget covers | |
| # cold uvicorn import + SQLite init. Fail loudly with the | |
| # last curl exit code so a stuck boot doesn't masquerade as | |
| # a flaky Playwright test downstream. | |
| run: | | |
| for i in $(seq 1 30); do | |
| if curl -sf http://127.0.0.1:7878/api/health > /dev/null; then | |
| echo "sidecar healthy after ${i}s" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "sidecar failed to come up within 30s" >&2 | |
| exit 1 | |
| - name: Run Playwright smoke | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| run: pnpm test:e2e | |
| - name: Stop sidecar | |
| # Run on failure too so a hung sidecar can't leak into a | |
| # subsequent job's resources. `|| true` because the process | |
| # may already be gone (e.g., killed by a SIGPIPE upstream). | |
| if: always() && steps.filter.outputs.dashboard == 'true' && env.SIDECAR_PID != '' | |
| run: kill "$SIDECAR_PID" 2>/dev/null || true | |
| - name: Upload Playwright artifacts on failure | |
| if: failure() && steps.filter.outputs.dashboard == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: | | |
| web/playwright-report/ | |
| web/test-results/ | |
| # 7-day retention is plenty for triaging a failed run; longer | |
| # would just burn artifact storage. | |
| retention-days: 7 | |
| dashboard-visual: | |
| name: Dashboard visual regression | |
| # Pixel baselines are environment-specific (fonts, anti-aliasing), so this | |
| # job runs ENTIRELY inside the pinned Playwright image — the SAME image used | |
| # to generate the committed baselines (scripts/update_visual_baselines.sh). | |
| # That keeps generation and assertion byte-identical and immune to the | |
| # `ubuntu-latest` runner image drifting underneath us. The behavioural | |
| # dashboard-e2e job above runs on the bare runner and `testIgnore`s | |
| # visual.spec.ts, so the two never compare pixels in mismatched environments. | |
| runs-on: ubuntu-latest | |
| needs: lint-and-unit | |
| container: | |
| # Must match `@playwright/test` in web/pnpm-lock.yaml AND the image tag in | |
| # scripts/update_visual_baselines.sh + playwright.visual.config.ts. Bump | |
| # all four together and regenerate baselines. | |
| image: mcr.microsoft.com/playwright:v1.60.0-noble | |
| # Build + uv sync + container deps + 72 screenshots; generous ceiling. | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Trust the workspace for git (container UID mismatch) | |
| # This is the ONLY job that runs INSIDE a container (the pinned | |
| # Playwright image); every other job uses the bare runner. git in | |
| # the container sees the /__w checkout as owned by a different UID | |
| # than the container's root user and aborts the first git op with | |
| # "dubious ownership" (exit 128) — which failed checkout/paths-filter | |
| # on every run since this job landed. actions/checkout only adds a | |
| # TEMPORARY safe.directory that it removes in its post step, so the | |
| # later steps still tripped. Writing to the SYSTEM config | |
| # (/etc/gitconfig) is HOME-independent and a trusted scope for | |
| # safe.directory, so every git invocation in the job — checkout, | |
| # paths-filter, the visual diff — trusts the tree. Runs before | |
| # checkout and unconditionally (it must precede the paths filter). | |
| run: git config --system --add safe.directory '*' | |
| - uses: actions/checkout@v6 | |
| - name: Detect dashboard-relevant changes | |
| id: filter | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| dashboard: | |
| - 'web/**' | |
| - 'schemabrain/dashboard/**' | |
| - 'scripts/dashboard_demo.py' | |
| - 'scripts/update_visual_baselines.sh' | |
| - '.github/workflows/ci.yml' | |
| - name: Install uv | |
| if: steps.filter.outputs.dashboard == 'true' | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: "uv.lock" | |
| - name: Set up Python 3.11 | |
| if: steps.filter.outputs.dashboard == 'true' | |
| run: uv python install 3.11 | |
| - name: Install Python dependencies (+ui extra) | |
| if: steps.filter.outputs.dashboard == 'true' | |
| run: uv sync --extra dev --extra ui --python 3.11 | |
| - name: Set up pnpm (corepack) | |
| # The Playwright image already ships Node; corepack activates the same | |
| # pnpm major as web/package.json engines (>=9), matching the local | |
| # generation script. No actions/setup-node (it would fight the image's | |
| # bundled Node). | |
| if: steps.filter.outputs.dashboard == 'true' | |
| run: corepack enable && corepack prepare pnpm@9 --activate | |
| - name: Cache pnpm store | |
| if: steps.filter.outputs.dashboard == 'true' | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.local/share/pnpm/store | |
| key: pnpm-visual-${{ runner.os }}-${{ hashFiles('web/pnpm-lock.yaml') }} | |
| - name: Install web dependencies | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| run: pnpm install --frozen-lockfile | |
| - name: Build dashboard static export | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| run: pnpm run export | |
| - name: Boot dashboard sidecar (headless) | |
| if: steps.filter.outputs.dashboard == 'true' | |
| # The sidecar and Playwright share this container's loopback, so the | |
| # config's default baseURL (127.0.0.1:7878) resolves with no extra | |
| # networking. | |
| run: | | |
| uv run --python 3.11 python scripts/dashboard_demo.py --no-open & | |
| echo "SIDECAR_PID=$!" >> "$GITHUB_ENV" | |
| - name: Wait for sidecar health | |
| if: steps.filter.outputs.dashboard == 'true' | |
| run: | | |
| for i in $(seq 1 30); do | |
| if curl -sf http://127.0.0.1:7878/api/health > /dev/null; then | |
| echo "sidecar healthy after ${i}s" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "sidecar failed to come up within 30s" >&2 | |
| exit 1 | |
| - name: Run visual regression (assert against committed baselines) | |
| if: steps.filter.outputs.dashboard == 'true' | |
| working-directory: web | |
| # Browsers are preinstalled in the image — no `playwright install`. A | |
| # diff fails the job; the report (with diff PNGs) uploads below. | |
| run: pnpm exec playwright test -c playwright.visual.config.ts | |
| - name: Stop sidecar | |
| if: always() && steps.filter.outputs.dashboard == 'true' && env.SIDECAR_PID != '' | |
| run: kill "$SIDECAR_PID" 2>/dev/null || true | |
| - name: Upload visual diff report on failure | |
| if: failure() && steps.filter.outputs.dashboard == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: visual-report | |
| path: | | |
| web/playwright-report/ | |
| web/test-results/ | |
| retention-days: 7 | |
| site-build: | |
| name: Marketing site (build + E2E) | |
| # The standalone marketing landing (site/) is a separate Next static-export | |
| # app deployed to Vercel — out of the pip wheel. Vercel owns the production | |
| # deploy; this job is the PR-gate that the site typechecks, lints, builds, | |
| # and passes its smoke. Gated on site/** so dashboard/docs/Python PRs skip | |
| # it. (Brand drift between web/ and site/ is caught separately by the | |
| # tests/test_brand_tokens_sync.py gate in lint-and-unit.) | |
| runs-on: ubuntu-latest | |
| needs: lint-and-unit | |
| timeout-minutes: 12 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Detect site-relevant changes | |
| id: filter | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| site: | |
| - 'site/**' | |
| - '.github/workflows/ci.yml' | |
| - name: Set up pnpm | |
| if: steps.filter.outputs.site == 'true' | |
| uses: pnpm/action-setup@v6 | |
| with: | |
| version: 9 | |
| - name: Set up Node 20 | |
| if: steps.filter.outputs.site == 'true' | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "pnpm" | |
| cache-dependency-path: site/pnpm-lock.yaml | |
| - name: Install site dependencies | |
| if: steps.filter.outputs.site == 'true' | |
| working-directory: site | |
| run: pnpm install --frozen-lockfile | |
| - name: Typecheck | |
| if: steps.filter.outputs.site == 'true' | |
| working-directory: site | |
| run: pnpm typecheck | |
| - name: Lint | |
| if: steps.filter.outputs.site == 'true' | |
| working-directory: site | |
| run: pnpm lint | |
| - name: Build static export | |
| if: steps.filter.outputs.site == 'true' | |
| working-directory: site | |
| run: pnpm build | |
| - name: Landing perf budget (bundle size + fonts swap) | |
| # Deterministic half of wsQA-perf-budget-landing for the marketing | |
| # landing: first-load JS < 150kb / CSS < 30kb (gzip, the Landing tier), | |
| # no reactflow, every fetched @font-face font-display: swap. Reads | |
| # site/.next from the build above. LCP/CLS = manual Lighthouse. | |
| if: steps.filter.outputs.site == 'true' | |
| run: node scripts/check_bundle_budget.mjs --app site --route '/page' --js 150 --css 30 --forbid-module 'react-flow__' --fonts-swap | |
| - name: Cache Playwright browsers | |
| if: steps.filter.outputs.site == 'true' | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-site-${{ runner.os }}-${{ hashFiles('site/pnpm-lock.yaml') }} | |
| - name: Install Playwright Chromium (+OS deps) | |
| if: steps.filter.outputs.site == 'true' | |
| working-directory: site | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Run Playwright smoke | |
| if: steps.filter.outputs.site == 'true' | |
| working-directory: site | |
| run: pnpm test:e2e | |
| - name: Upload Playwright artifacts on failure | |
| if: failure() && steps.filter.outputs.site == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: site-playwright-report | |
| path: | | |
| site/playwright-report/ | |
| site/test-results/ | |
| retention-days: 7 | |
| lockfree-install-smoke: | |
| name: Lock-free install smoke (fresh resolver) | |
| # Catches dependency-resolution drift that uv.lock hides. Every other | |
| # job installs via `uv sync`, which pins to uv.lock — but end users run | |
| # `pip install schemabrain`, which resolves dependency RANGES fresh | |
| # against PyPI. A wide upper bound (e.g. `sqlglot<27`, `fastapi<1.0`, | |
| # `anthropic<1.0`) resolving to a newer breaking release is invisible to | |
| # the locked suite and ships silently to users. (Concretely: sqlglot 30.x | |
| # renamed the `exp.Select` "from" arg key to "from_", silently emptying | |
| # join mining — CI stayed green on the locked 26.x while a fresh | |
| # `pip install` resolved the broken 30.x.) This job builds the wheel, | |
| # installs it into a clean venv with pip's resolver (NO uv.lock), and | |
| # smoke-tests the CLI, the MCP import surface, and the join-mining path | |
| # (mirrors tests/test_joins_mining.py::TestSqlglotVersionRegression). | |
| runs-on: ubuntu-latest | |
| needs: lint-and-unit | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Detect dependency / source changes | |
| id: filter | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| # Dependency ranges live in pyproject.toml; a source change can | |
| # also break import on a fresh resolve. Skip docs-only PRs to keep | |
| # the typical critical path fast. | |
| filters: | | |
| deps: | |
| - 'pyproject.toml' | |
| - 'schemabrain/**' | |
| - '.github/workflows/ci.yml' | |
| - name: Install uv | |
| if: steps.filter.outputs.deps == 'true' | |
| # Same point-tag pin as the rest of this workflow (see lint-and-unit | |
| # for the @v8-is-unsafe rationale). Used only to build the wheel and | |
| # provision Python — the install itself goes through pip so the | |
| # resolver matches what a `pip install` end user actually gets. | |
| uses: astral-sh/setup-uv@v8.1.0 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: "uv.lock" | |
| - name: Set up Python 3.11 | |
| if: steps.filter.outputs.deps == 'true' | |
| run: uv python install 3.11 | |
| - name: Build wheel | |
| # `--wheel` (not bare `uv build`) so bundled artifacts (eval golden | |
| # sets + SQL fixtures) ride along; the sdist target strips gitignored | |
| # artifacts. Matches the release packaging step in publish.yml. | |
| if: steps.filter.outputs.deps == 'true' | |
| run: uv build --wheel | |
| - name: Install the wheel with a FRESH pip resolver (no uv.lock) | |
| # A clean, seeded venv + `pip install` of the built wheel resolves | |
| # every dependency range against PyPI from scratch — exactly the path | |
| # `pip install schemabrain[ui]` takes for an end user. The `[ui]` | |
| # extra is included so the fastapi/uvicorn/sse-starlette upper bounds | |
| # resolve fresh too, not just the core deps. The wheel filename is | |
| # captured into a var so the `[ui]` extra isn't mangled by shell glob | |
| # bracket-expansion. | |
| if: steps.filter.outputs.deps == 'true' | |
| run: | | |
| uv venv --seed --python 3.11 "$RUNNER_TEMP/freshenv" | |
| WHEEL=$(ls dist/schemabrain-*.whl | head -1) | |
| "$RUNNER_TEMP/freshenv/bin/pip" install "${WHEEL}[ui]" | |
| - name: Smoke the shipped wheel | |
| # Run from $RUNNER_TEMP so the repo's source tree is NOT on sys.path: | |
| # `import schemabrain` then resolves to the installed wheel, not the | |
| # checkout. Asserts the CLI entry point works, the MCP import surface | |
| # loads, and join mining resolves the FROM-side table — the exact path | |
| # sqlglot 30.x silently empties. A drift here fails the build loudly | |
| # instead of shipping an empty join-mining path to users. | |
| if: steps.filter.outputs.deps == 'true' | |
| run: | | |
| cd "$RUNNER_TEMP" | |
| "$RUNNER_TEMP/freshenv/bin/schemabrain" --version | |
| "$RUNNER_TEMP/freshenv/bin/python" - <<'PY' | |
| import sqlglot | |
| print("resolved sqlglot:", sqlglot.__version__) | |
| from schemabrain.mcp.server import build_server | |
| assert callable(build_server), "MCP build_server is not importable" | |
| from schemabrain.joins.mining import extract_join_predicates | |
| sql = "SELECT * FROM public.users u JOIN public.orders o ON u.id = o.user_id" | |
| result = extract_join_predicates(sql) | |
| assert result, ( | |
| "join mining returned an empty set on a fresh resolve — likely a " | |
| "sqlglot major bump renaming the FROM arg key (30.x: 'from'->'from_'); " | |
| "see the sqlglot pin in pyproject.toml" | |
| ) | |
| tables = {t for j in result for t in (j.table_a, j.table_b)} | |
| assert tables == {"orders", "users"}, f"unexpected tables resolved: {tables}" | |
| print("lock-free smoke OK — CLI, MCP import, and join mining all green") | |
| PY |