fix(deps): bump the production-deps group with 7 updates #199
Workflow file for this run
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
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: read-all | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| # Classifies the diff so docs-only changes can short-circuit downstream jobs | |
| # without using top-level `paths-ignore` (which makes required status checks | |
| # stall on docs-only PRs because the workflow never runs). | |
| changes: | |
| name: Detect changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| outputs: | |
| code: ${{ steps.filter.outputs.code }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - id: filter | |
| uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| with: | |
| filters: | | |
| code: | |
| - '!**/*.md' | |
| - '!LICENSE' | |
| - '!.gitignore' | |
| - '!.prettierignore' | |
| lint: | |
| name: Lint, Format & Audit | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Format check | |
| run: pnpm format:check | |
| - name: Security audit | |
| # Runs `pnpm audit` against production deps and fails CI only on | |
| # high/critical advisories that have a known fix (`patched_versions` | |
| # set, non-empty, not "<0.0.0"). | |
| run: | | |
| set -u | |
| tmp=$(mktemp) | |
| trap 'rm -f "$tmp"' EXIT | |
| pnpm audit --json --audit-level=high --prod > "$tmp" 2>/dev/null || true | |
| if ! jq empty "$tmp" >/dev/null 2>&1; then | |
| echo "::warning::pnpm audit produced non-JSON output (likely registry error). Skipping." | |
| cat "$tmp" | |
| exit 0 | |
| fi | |
| total=$(jq '[.advisories[]? | select(.severity == "high" or .severity == "critical")] | length' "$tmp") | |
| fixable_count=$(jq '[.advisories[]? | select((.severity == "high" or .severity == "critical") and (.patched_versions // "") != "" and (.patched_versions // "") != "<0.0.0")] | length' "$tmp") | |
| echo "high/critical advisories: $total — fixable: $fixable_count" | |
| if [ "$fixable_count" -gt 0 ]; then | |
| echo "::error::$fixable_count high/critical vulnerabilities have an available fix — bumping is required" | |
| jq '.advisories[]? | select((.severity == "high" or .severity == "critical") and (.patched_versions // "") != "" and (.patched_versions // "") != "<0.0.0") | {id, module_name, severity, vulnerable_versions, patched_versions, title, url}' "$tmp" | |
| exit 1 | |
| fi | |
| if [ "$total" -gt 0 ]; then | |
| echo "::warning::$total high/critical advisories present with no fix available yet — not blocking CI" | |
| jq '.advisories[]? | select(.severity == "high" or .severity == "critical") | {id, module_name, severity, title, url}' "$tmp" | |
| fi | |
| commitlint: | |
| name: Commitlint | |
| needs: changes | |
| if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - uses: ./.github/actions/setup | |
| - name: Validate PR commit messages | |
| run: | | |
| pnpm exec commitlint \ | |
| --from "${{ github.event.pull_request.base.sha }}" \ | |
| --to "${{ github.event.pull_request.head.sha }}" \ | |
| --verbose | |
| typecheck-build: | |
| name: Typecheck & Build | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # Prisma 7's prisma.config.ts resolves `env('DATABASE_URL')` even at | |
| # `prisma generate` time. Generate doesn't actually connect, so a | |
| # well-formed placeholder is enough, the URL never reaches a server. | |
| env: | |
| DATABASE_URL: postgresql://placeholder:placeholder@localhost:5432/placeholder | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup | |
| - name: Generate Prisma client | |
| run: pnpm --filter backend exec prisma generate | |
| - name: Typecheck | |
| run: pnpm typecheck | |
| - name: Build | |
| run: pnpm build | |
| test: | |
| name: Tests (${{ matrix.package }}) | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: [backend, frontend] | |
| services: | |
| postgres: | |
| image: postgres:18-alpine | |
| env: | |
| POSTGRES_USER: article30 | |
| POSTGRES_PASSWORD: article30_secret | |
| POSTGRES_DB: article30 | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U article30 -d article30" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| redis: | |
| image: redis:8-alpine | |
| env: | |
| REDIS_PASSWORD: article30_redis_dev | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli -a article30_redis_dev ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| env: | |
| # Test-only credentials that match the services above. | |
| DATABASE_URL: postgresql://article30:article30_secret@localhost:5432/article30 | |
| DATABASE_URL_TEST: postgresql://article30:article30_secret@localhost:5432/article30_test | |
| POSTGRES_ADMIN_URL_TEST: postgresql://article30:article30_secret@localhost:5432/postgres | |
| REDIS_URL: redis://localhost:6379 | |
| REDIS_PASSWORD: article30_redis_dev | |
| AUDIT_HMAC_SECRET: ci-only-audit-secret-not-for-production | |
| SESSION_SECRET: ci-only-session-secret-not-for-production | |
| NODE_ENV: test | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup | |
| - name: Install psql client | |
| if: matrix.package == 'backend' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq postgresql-client | |
| - name: Build shared package | |
| run: pnpm --filter @article30/shared build | |
| - name: Generate Prisma client | |
| if: matrix.package == 'backend' | |
| run: pnpm --filter backend exec prisma generate | |
| - name: Run tests with coverage | |
| run: pnpm --filter @article30/${{ matrix.package }} test:coverage | |
| - name: Upload coverage to Codecov | |
| if: always() | |
| uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 | |
| with: | |
| files: ${{ matrix.package }}/coverage/lcov.info | |
| flags: ${{ matrix.package }} | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload coverage HTML report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.package }}-coverage-html | |
| path: ${{ matrix.package }}/coverage/ | |
| retention-days: 14 | |
| # Rollup of the `test` matrix - emits a single check named "Tests" so the | |
| # branch-protection ruleset can require one stable name regardless of how | |
| # the matrix grows. Adding a new matrix leg (shared, integration, etc.) | |
| # will not require touching the ruleset. | |
| tests: | |
| name: Tests | |
| needs: [changes, test] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Aggregate matrix outcome | |
| env: | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| TEST_RESULT: ${{ needs.test.result }} | |
| run: | | |
| echo "changes: $CHANGES_RESULT, test: $TEST_RESULT" | |
| # `changes` must succeed - it's the gate; if it failed we don't | |
| # know whether tests should have run. `test` must be either | |
| # success (all legs passed) or skipped (legitimately gated off | |
| # for a docs-only PR by `code == 'false'`). | |
| if [ "$CHANGES_RESULT" != "success" ]; then | |
| echo "::error::changes job did not succeed" | |
| exit 1 | |
| fi | |
| case "$TEST_RESULT" in | |
| success|skipped) exit 0 ;; | |
| *) echo "::error::test matrix result: $TEST_RESULT"; exit 1 ;; | |
| esac | |
| e2e: | |
| name: E2E (Playwright) | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| services: | |
| postgres: | |
| image: postgres:18-alpine | |
| env: | |
| POSTGRES_USER: article30 | |
| POSTGRES_PASSWORD: article30_secret | |
| POSTGRES_DB: article30 | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U article30 -d article30" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| redis: | |
| image: redis:8-alpine | |
| env: | |
| REDIS_PASSWORD: article30_redis_dev | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli -a article30_redis_dev ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| env: | |
| DATABASE_URL: postgresql://article30:article30_secret@localhost:5432/article30 | |
| REDIS_URL: redis://localhost:6379 | |
| REDIS_PASSWORD: article30_redis_dev | |
| NODE_ENV: test | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: ./.github/actions/setup | |
| - name: Install psql client | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq postgresql-client | |
| - name: Generate Prisma client | |
| run: pnpm --filter @article30/backend exec prisma generate | |
| - name: Resolve Playwright version | |
| id: playwright-version | |
| # Cache key is scoped to the resolved @playwright/test version so a | |
| # pnpm-lock.yaml bump (new browser snapshot) invalidates the cache | |
| # cleanly. Frontend depends on @playwright/test, not bare `playwright` | |
| # — under pnpm that's the path that actually exists. | |
| run: | | |
| VERSION=$(node -p "require('./frontend/node_modules/@playwright/test/package.json').version") | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Cache Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} | |
| - name: Install Playwright browsers (cache miss) | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: pnpm --filter @article30/frontend exec playwright install chromium --with-deps | |
| - name: Install Playwright system deps (cache hit) | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| run: pnpm --filter @article30/frontend exec playwright install-deps chromium | |
| - name: Build frontend + backend | |
| run: pnpm build | |
| - name: Run E2E | |
| run: pnpm --filter @article30/frontend e2e | |
| - name: Upload Playwright report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-report | |
| path: frontend/playwright-report/ | |
| retention-days: 14 | |
| - name: Upload Playwright traces | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-traces | |
| path: frontend/test-results/ | |
| retention-days: 7 | |
| docker: | |
| name: Docker Build | |
| needs: changes | |
| if: needs.changes.outputs.code == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup Docker Buildx | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| # Persists the BuildKit `id=pnpm` cache mount used by both Dockerfiles | |
| # across CI runs. Without this, a `pnpm-lock.yaml` bump invalidates the | |
| # `pnpm install` layer and the store gets re-downloaded from scratch | |
| # (cause of the 19m frontend-image outlier observed on release commits). | |
| - name: Cache pnpm store (for BuildKit) | |
| id: pnpm-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: pnpm-cache | |
| key: pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} | |
| restore-keys: | | |
| pnpm-store- | |
| - name: Inject pnpm store cache into BuildKit | |
| uses: reproducible-containers/buildkit-cache-dance@5422eac04292c961a382e0f584ea0f03ad9da723 # v3.4.0 | |
| with: | |
| cache-map: | | |
| { | |
| "pnpm-cache": "/pnpm/store" | |
| } | |
| skip-extraction: ${{ steps.pnpm-cache.outputs.cache-hit }} | |
| - name: Build backend image | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: . | |
| file: backend/Dockerfile | |
| push: false | |
| load: false | |
| cache-from: type=gha,scope=backend | |
| cache-to: type=gha,mode=max,scope=backend | |
| - name: Build frontend image | |
| uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 | |
| with: | |
| context: . | |
| file: frontend/Dockerfile | |
| push: false | |
| load: false | |
| cache-from: type=gha,scope=frontend | |
| cache-to: type=gha,mode=max,scope=frontend |