Skip to content

chore(ci)(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 in the actions-minor-and-patch group across 1 directory #685

chore(ci)(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 in the actions-minor-and-patch group across 1 directory

chore(ci)(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 in the actions-minor-and-patch group across 1 directory #685

Workflow file for this run

# 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.2.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.2.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.2.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.2.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.2.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.2.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