Skip to content

chore(deps-dev): bump @next/bundle-analyzer from 16.3.4 to 16.3.5 in /web #1270

chore(deps-dev): bump @next/bundle-analyzer from 16.3.4 to 16.3.5 in /web

chore(deps-dev): bump @next/bundle-analyzer from 16.3.4 to 16.3.5 in /web #1270

Workflow file for this run

# GitHub Actions: lint + type-check + test the gw2analytics monorepo.
#
# Split into parallel jobs for faster feedback:
# lint-python ──> test-python (needs postgres)
# lint-web ──> playwright (chromium + visual-regression)
# arq-integration (parallel, needs postgres + redis)
#
# Previously a single monolithic ``lint-and-test`` job ran all 16+
# steps sequentially (~12-15 min wall clock). The parallel split
# brings the critical path to ~6-7 min by running Python lint, web
# lint, and ARQ integration simultaneously, with heavier test/e2e
# jobs starting immediately after their respective lint gates.
#
# Uses local composite actions (.github/actions/setup-*) to keep
# setup blocks DRY.
#
# pytest-env injects the docker-compose dev credentials at session
# startup, so this workflow does not need any GitHub repository
# secrets. A ``postgres`` service runs alongside the test-python job
# so the end-to-end ``apps/api/tests/test_uploads_e2e.py`` test can
# hit a real database at ``localhost:5432``.
name: CI
on:
# v0.15.2: repo flipped from private → public (LICENSE-cleanup
# series), so the ``spending_limit`` block no longer applies and
# auto-triggers are restored. ``push`` / ``pull_request`` cover
# normal development; ``workflow_dispatch`` stays for ad-hoc
# manual runs (e.g. cache-warmup, debugging a single job).
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# Minimum-privilege token default for the public-repo era. Without
# this, the GITHUB_TOKEN inherits the repo's default permissions
# (potentially ``write-all`` on legacy settings), which is a pivot
# risk if a malicious PR ever exfiltrates it. See GitHub docs:
# https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
# Cancel PR runs on rapid-fire pushes to save contributor minutes,
# but let ``main`` runs complete to preserve CI history for each
# landed commit (so a flaky rerun doesn't lose visibility).
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# ---------------------------------------------------------------------------
# Python lint: ruff, mypy, and security audit.
# Uses the full workspace sync (setup-python composite action) so
# workspace members are available as editable packages for mypy.
# ---------------------------------------------------------------------------
jobs:
lint-python:
name: Lint Python
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-python
# Cache pip-audit's OSV vulnerability database so subsequent
# runs skip the ~15s download on every invocation.
- name: Cache pip-audit OSV data
uses: actions/cache@v4
with:
path: ~/.cache/pip-audit
key: pip-audit-${{ hashFiles('uv.lock') }}
- name: Ruff check
run: uv run ruff check --output-format=concise
- name: Ruff format check
run: uv run ruff format --check
# v0.13.9 guard: block reintroduction of legacy db.query()
- name: Guard against legacy db.query()
run: |
echo "Checking for legacy db.query() calls (modern select() required)..."
! grep -rn 'db\.query(' apps/api/src/ libs/ --include='*.py'
- name: Mypy
# Split into two invocations so libs/__main__.py and
# apps/__main__.py don't collide (each invocation only
# discovers one __main__.py).
run: |
uv run mypy libs --no-incremental
uv run mypy apps/api/src --no-incremental
- name: pip-audit (Python deps, vulnerability scan)
# ponytail: suppress known pre-existing CVEs (protobuf PYSEC-2026-1805,
# setuptools PYSEC-2026-3447). These are transitive deps pinned by
# opentelemetry & OTel instrumentation compatibility constraints.
run: uv run pip-audit --strict --vulnerability-service osv --ignore-vuln PYSEC-2026-1805 --ignore-vuln PYSEC-2026-3447
- name: Lint summary
# Only on success to avoid showing ✅ when steps actually failed.
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🔍 Python lint results
| Check | Status |
|-------|--------|
| Ruff check | ✅ |
| Ruff format | ✅ |
| db.query() guard | ✅ |
| Mypy | ✅ |
| pip-audit | ✅ |
EOF
# ---------------------------------------------------------------------------
# Python tests: DB-backed pytest suite.
# Depends on lint-python so lint failures abort before running 4 min of tests.
# ---------------------------------------------------------------------------
test-python:
name: Test Python
needs: lint-python
runs-on: ubuntu-latest
env:
DATABASE_URL: "postgresql+psycopg://gw2analytics:gw2analytics@localhost:5432/gw2analytics"
S3_ENDPOINT: "localhost:9000"
S3_ACCESS_KEY: "gw2analytics"
S3_SECRET_KEY: "gw2analytics-secret"
S3_BUCKET: "gw2analytics"
# NOTE: SECRETS_KEK is hardcoded to the deterministic
# test-fixture value (matches pyproject.toml
# ``[tool.pytest_env]``). Rationale:
# (a) pytest-env injects this DURING pytest runtime, but
# the alembic ``env.py`` instantiates Settings()
# which requires SECRETS_KEK -- and alembic runs
# BEFORE pytest, so the env block MUST provide it
# explicitly. pytest-env alone is insufficient.
# (b) ``${{ secrets.SECRETS_KEK }}`` would BREAK fork
# PRs (GitHub strips secrets from forks -> empty
# string -> Fernet crashes with ``ValueError: Fernet
# key must be 32 url-safe base64-encoded bytes.``).
# (c) The dummy value is already public in
# pyproject.toml (# ``base64.urlsafe_b64encode(b"a"*32)``),
# so hardcoding here leaks no additional information.
SECRETS_KEK: "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: gw2analytics
POSTGRES_PASSWORD: gw2analytics
POSTGRES_DB: gw2analytics
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U gw2analytics"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-python
- name: Start MinIO
run: |
docker run -d --name minio \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=gw2analytics \
-e MINIO_ROOT_PASSWORD=gw2analytics-secret \
minio/minio:latest server /data --console-address :9001
echo "Waiting for MinIO to be ready..."
for i in $(seq 1 15); do
if curl -sf http://localhost:9000/minio/health/live > /dev/null 2>&1; then
echo "MinIO is ready!"
break
fi
sleep 2
done
- name: Run database migrations
working-directory: apps/api
run: uv run alembic upgrade head
- name: Health probe baseline
run: uv run python -m gw2analytics_api.scripts.health_gate --save-baseline /tmp/health_baseline.json
- name: Script tests
run: uv run pytest tests/scripts/ --tb=line -q
- name: Pytest
# --cov-report=xml generates coverage.xml for Codecov ingestion.
run: uv run pytest --tb=line -q --cov-report=xml
- name: Upload coverage to Codecov
# Only on main (PRs don't have the CODECOV_TOKEN secret).
# Uses codecov-action@v5 which gracefully skips upload when
# no token is available (e.g. on PRs from forks).
if: github.ref == 'refs/heads/main'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
flags: python
fail_ci_if_error: false
- name: Health probe CI gate (regression check)
run: uv run python -m gw2analytics_api.scripts.health_gate --check-delta /tmp/health_baseline.json
- name: Cleanup /tmp/health_baseline.json
if: always()
run: rm -f /tmp/health_baseline.json
- name: Test summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🧪 Python test results
| Step | Status |
|------|--------|
| Database migrations | ✅ |
| Health probe baseline | ✅ |
| Script tests | ✅ |
| Pytest | ✅ |
| Codecov upload | ✅ |
| Health probe gate | ✅ |
EOF
# ---------------------------------------------------------------------------
# Web lint: OpenAPI codegen, TypeScript type-check, vitest, and pnpm audit.
# Needs Python (for dump_openapi.py) + Node/pnpm.
# ---------------------------------------------------------------------------
lint-web:
name: Lint Web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
# Python setup for the OpenAPI codegen step (dump_openapi.py)
- uses: ./.github/actions/setup-python
- name: Dump FastAPI OpenAPI spec
run: uv run python web/scripts/dump_openapi.py > /tmp/openapi.json
# Node + pnpm for the web toolchain
- uses: ./.github/actions/setup-web
- name: Regenerate web TypeScript client from OpenAPI spec
working-directory: web
run: pnpm exec openapi-typescript /tmp/openapi.json -o src/lib/api/schema.d.ts
- name: Detect API client drift
run: git diff --exit-code -- web/src/lib/api/schema.d.ts
- name: Type-check web
working-directory: web
run: pnpm exec tsc --noEmit
- name: Web unit tests (vitest)
working-directory: web
run: pnpm exec vitest run --coverage --reporter=verbose
- name: Upload vitest coverage to Codecov
if: github.ref == 'refs/heads/main'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: web/coverage/lcov.info
flags: javascript
fail_ci_if_error: false
- name: pnpm audit (Node deps, fail on HIGH)
working-directory: web
# v0.16.3: suppress known pre-existing advisory GHSA-mh99-v99m-4gvg
# (brace-expansion, a transitive dev-dep DoS vulnerability that
# does not affect the production application). Remove the --ignore
# flag once the transitive dependency chain is updated past
# brace-expansion@5.0.7.
run: pnpm audit --audit-level=high --ignore GHSA-mh99-v99m-4gvg
- name: Cleanup /tmp/openapi.json
if: always()
run: rm -f /tmp/openapi.json
- name: Web lint summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🌐 Web lint results
| Check | Status |
|-------|--------|
| OpenAPI codegen | ✅ |
| API client drift | ✅ |
| TypeScript type-check | ✅ |
| Vitest unit tests | ✅ |
| Vitest coverage upload | ✅ |
| pnpm audit | ✅ |
EOF
# ---------------------------------------------------------------------------
# Playwright E2E tests (chromium).
# Runs on every push/PR. Browser cache (~/.cache/ms-playwright) and
# the cache-warmup workflow mitigate setup cost.
# ---------------------------------------------------------------------------
playwright-chromium:
name: Playwright (chromium)
needs: lint-web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/setup-web
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('web/pnpm-lock.yaml') }}
- name: Install Playwright chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: web
run: pnpm exec playwright install chromium
- name: Install Playwright system dependencies
working-directory: web
run: pnpm exec playwright install-deps chromium
- name: Playwright tests (chromium)
working-directory: web
run: pnpm exec playwright test --project=chromium
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-chromium
path: web/playwright-report/
retention-days: 7
- name: Upload Playwright traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces-chromium
path: web/test-results/
retention-days: 7
- name: Playwright summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🎭 Playwright (chromium) results
| Check | Status |
|-------|--------|
| E2E tests | ✅ |
EOF
# ---------------------------------------------------------------------------
# Playwright visual regression tests.
# PR-only because there's no baseline to compare against on main.
# ---------------------------------------------------------------------------
playwright-visual-regression:
name: Playwright (visual-regression)
needs: lint-web
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
# v0.16.x: the committed PNG baselines at docs/screenshots/
# are from a pre-v0.16.x commit era (last touched 2026-07-07
# to 2026-07-15, ~10 days before the 2026-07-24 release;
# verifiable directly via
# ``git log -1 --format=%H -- docs/screenshots/<file>`` --
# see the "Exit criterion" block below for the verbatim
# recipe). Real diffs vs the committed baselines are NOT
# regressions -- they are baseline staleness. Until a
# follow-up PR refreshes the 9 baselines via a CI-equivalent
# standalone prod build, the ``continue-on-error: true`` flag
# stops this job from blocking PR merges while still running
# the suite (and uploading the diff artifacts via the
# ``failure()`` conditionals below -- they're still useful for
# whoever refreshes the baselines next). Local devs retain
# the same test signal via
# ``pnpm exec playwright test --project=visual-regression``.
#
# Why ``continue-on-error: true`` is sufficient as a gate:
# the flag only prevents workflow FAILURE on this job's
# failure; the underlying status check still reports ``failure``.
# It is currently safe because CONTRIBUTING.md §"Branch
# protection for main" lists 6 required-status-checks (see
# that section for the current list), and ``Playwright
# (visual-regression)`` is NOT among them -- so a failed run
# here does NOT block the merge gate. WARNING: a future
# maintainer who adds this job to the required_status_checks
# ruleset MUST also remove ``continue-on-error: true`` (or the
# gate will silently start blocking merges). Cite
# CONTRIBUTING.md "Branch protection for main" by section
# when revisiting.
#
# Exit criterion: remove this ``continue-on-error: true`` flag
# in a follow-up commit once (a) the docs/screenshots/*.png
# baselines have been refreshed against the v0.16.x UI via a
# CI-hosted capture (NOT a developer laptop -- host-specific
# font/chromium drift will re-introduce the diff); AND (b) 5+
# consecutive green main-push runs after the refresh confirm
# the strict 1.5% threshold is no longer flaky. The flag
# exists ONLY because the committed baselines are pre-v0.16.x;
# verifying baseline provenance is via
# ``git log -1 --format=%H -- docs/screenshots/<file>``.
continue-on-error: true
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/setup-web
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('web/pnpm-lock.yaml') }}
- name: Install Playwright chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: web
run: pnpm exec playwright install chromium
- name: Install Playwright system dependencies
working-directory: web
run: pnpm exec playwright install-deps chromium
- name: Playwright tests (visual-regression)
working-directory: web
run: pnpm exec playwright test --project=visual-regression
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-visual-regression
path: web/playwright-report/
retention-days: 7
- name: Upload Playwright traces on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces-visual-regression
path: web/test-results/
retention-days: 7
- name: Upload visual regression diffs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-regression-diffs
path: web/tests/e2e/.visual-regression-output/
retention-days: 7
- name: Playwright summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## 🎭 Playwright (visual-regression) results
| Check | Status |
|-------|--------|
| Visual regression tests | ✅ |
EOF
# ---------------------------------------------------------------------------
# ARQ worker integration tests (parallel, needs postgres + redis)
# ---------------------------------------------------------------------------
arq-integration:
name: ARQ worker integration
runs-on: ubuntu-latest
env:
DATABASE_URL: "postgresql+psycopg://gw2analytics:gw2analytics@localhost:5432/gw2analytics"
S3_ENDPOINT: "localhost:9000"
S3_ACCESS_KEY: "gw2analytics"
S3_SECRET_KEY: "gw2analytics-secret"
S3_BUCKET: "gw2analytics"
# See test-python env block above for the full rationale.
# pytest-env alone cannot satisfy the alembic-pre-pytest
# injection window, so the test-fixture value is hardcoded
# here too.
SECRETS_KEK: "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
ARQ_REDIS_HOST: localhost
ARQ_REDIS_PORT: 6379
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: gw2analytics
POSTGRES_PASSWORD: gw2analytics
POSTGRES_DB: gw2analytics
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U gw2analytics"
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ./.github/actions/setup-python
- name: Start MinIO
run: |
docker run -d --name minio \
-p 9000:9000 -p 9001:9001 \
-e MINIO_ROOT_USER=gw2analytics \
-e MINIO_ROOT_PASSWORD=gw2analytics-secret \
minio/minio:latest server /data --console-address :9001
echo "Waiting for MinIO to be ready..."
for i in $(seq 1 15); do
if curl -sf http://localhost:9000/minio/health/live > /dev/null 2>&1; then
echo "MinIO is ready!"
break
fi
sleep 2
done
- name: Run database migrations
working-directory: apps/api
run: uv run alembic upgrade head
- name: ARQ worker integration tests
working-directory: apps/api
run: uv run pytest tests/test_parser_worker.py tests/test_uploads_arq.py -v
- name: ARQ summary
if: success()
run: |
cat <<'EOF' >> $GITHUB_STEP_SUMMARY
## ⚡ ARQ worker integration results
| Check | Status |
|-------|--------|
| Database migrations | ✅ |
| ARQ worker tests | ✅ |
EOF
# ---------------------------------------------------------------------------
# DCO sign-off check.
# PR-only. GitHub's web-DCO toggle (separate repo setting) covers
# github.com web-editor commits natively; this inline check covers
# CLI/IDE commits via ``git commit -s`` per the DCO model
# documented in CONTRIBUTING.md.
#
# Implemented as plain bash (``git log`` + ``grep``) so there is no
# external action dependency — the previous ``crazy-max/ghaction-dco@v1``
# was removed upstream, breaking the check silently.
#
# NOT currently added to the branch-protection ``required_status_checks``
# rule — we ship it informational-only first. Bump to hard-required
# after one real Contributor-signed PR has demonstrated the workflow
# end-to-end. The job will FAIL (not error) on unsigned commits, so the
# first failing PR will surface clearly in the Actions UI without
# blocking the merge until we wire it into required_status_checks.
# ---------------------------------------------------------------------------
dco-check:
name: DCO check
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Full history needed so ``git log <base>..<head>`` resolves.
fetch-depth: 0
persist-credentials: false
# Inline DCO check: iterates every commit in the PR range and
# verifies a ``Signed-off-by:`` trailer exists in its body.
# Avoids any third-party action dependency.
- name: Run DCO check
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set +e
bad=0
for commit in $(git log --format='%H' "${BASE_SHA}..${HEAD_SHA}"); do
if ! git log --format='%B' -1 "$commit" | grep -qi 'Signed-off-by:'; then
echo "::error::Missing Signed-off-by: trailer in commit ${commit}"
bad=$((bad+1))
fi
done
if [ $bad -gt 0 ]; then
echo "::warning::${bad} commit(s) missing Signed-off-by: trailer — DCO check FAILED"
exit 1
fi
echo "All commits have a Signed-off-by: trailer — DCO check PASSED"