ci(deps): bump the github-actions-all group across 1 directory with 3… #13
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: | |
| - "**" | |
| paths-ignore: | |
| - "**.md" | |
| - ".gitignore" | |
| - "docs/**" | |
| pull_request: | |
| branches: [main, develop] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| NODE_VERSION: "24" | |
| jobs: | |
| # ── Detect project state ──────────────────────────────────────────────────── | |
| # Three-tier model — CI never fails on a fresh template. | |
| # | |
| # Tier 1 — No pnpm-lock.yaml → all checks skipped | |
| # Tier 2 — Lock present, no vite.config → security audit only | |
| # Tier 3 — Lock + vite.config present → full pipeline | |
| # | |
| # Optional tool detection — jobs that depend on tools not yet installed | |
| # by the developer are skipped gracefully rather than failing: | |
| # has-vitest → Unit Tests job | |
| # has-playwright → E2E Tests job | |
| # ─────────────────────────────────────────────────────────────────────────── | |
| detect: | |
| name: Detect project state | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has-lockfile: ${{ steps.check.outputs.has-lockfile }} | |
| has-vite: ${{ steps.check.outputs.has-vite }} | |
| has-vitest: ${{ steps.check.outputs.has-vitest }} | |
| has-playwright: ${{ steps.check.outputs.has-playwright }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Check for project files | |
| id: check | |
| run: | | |
| echo "──────────────────────────────────────────" | |
| echo " Detecting project state..." | |
| echo "──────────────────────────────────────────" | |
| if [ -f "pnpm-lock.yaml" ]; then | |
| echo "has-lockfile=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ pnpm-lock.yaml found" | |
| else | |
| echo "has-lockfile=false" >> "$GITHUB_OUTPUT" | |
| echo "ℹ️ No pnpm-lock.yaml — Tier 1: all checks skipped" | |
| fi | |
| if [ -f "vite.config.ts" ]; then | |
| echo "has-vite=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ vite.config.ts found — Tier 3: full CI active" | |
| else | |
| echo "has-vite=false" >> "$GITHUB_OUTPUT" | |
| echo "ℹ️ No vite.config.ts — Tier 2: pre-scaffold CI only" | |
| fi | |
| if [ -f "pnpm-lock.yaml" ] && grep -q '"vitest"' pnpm-lock.yaml 2>/dev/null; then | |
| echo "has-vitest=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ vitest found in lockfile" | |
| else | |
| echo "has-vitest=false" >> "$GITHUB_OUTPUT" | |
| echo "ℹ️ No vitest — unit tests will be skipped" | |
| fi | |
| if [ -f "pnpm-lock.yaml" ] && grep -q '"@playwright/test"' pnpm-lock.yaml 2>/dev/null; then | |
| echo "has-playwright=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ @playwright/test found in lockfile" | |
| else | |
| echo "has-playwright=false" >> "$GITHUB_OUTPUT" | |
| echo "ℹ️ No @playwright/test — e2e will be skipped" | |
| fi | |
| echo "" | |
| echo "Summary:" | |
| echo " pnpm-lock.yaml : $([ -f pnpm-lock.yaml ] && echo 'YES' || echo 'NO')" | |
| echo " vite.config.ts : $([ -f vite.config.ts ] && echo 'YES' || echo 'NO')" | |
| echo " vitest : $(grep -q '"vitest"' pnpm-lock.yaml 2>/dev/null && echo 'YES' || echo 'NO')" | |
| echo " @playwright/test : $(grep -q '"@playwright/test"' pnpm-lock.yaml 2>/dev/null && echo 'YES' || echo 'NO')" | |
| # ── Tier 3 only ───────────────────────────────────────────────────────────── | |
| typecheck: | |
| name: Type Check (tsc) | |
| runs-on: ubuntu-latest | |
| needs: detect | |
| if: > | |
| needs.detect.outputs.has-lockfile == 'true' && | |
| needs.detect.outputs.has-vite == 'true' && | |
| (github.event.pull_request.draft == false || github.event_name != 'pull_request') | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm exec tsc --noEmit | |
| lint: | |
| name: Lint & Format (Biome) | |
| runs-on: ubuntu-latest | |
| needs: detect | |
| if: > | |
| needs.detect.outputs.has-lockfile == 'true' && | |
| needs.detect.outputs.has-vite == 'true' && | |
| (github.event.pull_request.draft == false || github.event_name != 'pull_request') | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - run: pnpm install --frozen-lockfile | |
| # biome check covers both lint AND format in Biome v2 — | |
| # the separate `biome format --check` step was removed because | |
| # the --check flag no longer exists in Biome v2. | |
| - run: pnpm exec biome check . | |
| # ── Unit Tests + fan-in ────────────────────────────────────────────────────── | |
| # This job runs after lint and serves two purposes: | |
| # | |
| # 1. Runs unit tests when vitest is installed. | |
| # 2. Acts as the fan-in gate for build — it always runs (if: always() + | |
| # lint success) so build never gets skipped due to a skipped test job. | |
| # | |
| # Why combined? GitHub Actions skips downstream jobs when ANY job in their | |
| # `needs` list was skipped — even with explicit `if` conditions. The only | |
| # reliable fix is to never put an optional job in `needs`. By absorbing | |
| # both test and fan-in into one job we keep the `needs` chain clean. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| test: | |
| name: Unit Tests (Vitest) | |
| runs-on: ubuntu-latest | |
| needs: [detect, typecheck, lint] | |
| if: > | |
| always() && | |
| needs.typecheck.result == 'success' && | |
| needs.lint.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - run: pnpm install --frozen-lockfile | |
| - name: Run unit tests | |
| run: | | |
| if [ "${{ needs.detect.outputs.has-vitest }}" == "true" ]; then | |
| echo "✅ vitest detected — running tests" | |
| pnpm exec vitest run --coverage | |
| else | |
| echo "⏭ vitest not installed — skipping unit tests" | |
| echo " Install vitest when you are ready to write tests:" | |
| echo " pnpm add -D vitest @vitest/coverage-v8" | |
| fi | |
| - name: Upload coverage | |
| uses: codecov/codecov-action@v7 | |
| if: needs.detect.outputs.has-vitest == 'true' | |
| with: | |
| files: ./coverage/lcov.info | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| build: | |
| name: Build (Vite) | |
| runs-on: ubuntu-latest | |
| needs: test | |
| if: needs.test.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - run: pnpm install --frozen-lockfile | |
| - run: pnpm exec vite build | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dist | |
| path: dist/ | |
| retention-days: 7 | |
| e2e: | |
| name: E2E Tests (Playwright) | |
| runs-on: ubuntu-latest | |
| needs: [detect, build] | |
| # Guard: only run if @playwright/test is actually installed. | |
| if: > | |
| needs.build.result == 'success' && | |
| needs.detect.outputs.has-playwright == 'true' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - run: pnpm install --frozen-lockfile | |
| - name: Install Playwright browsers | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Run E2E tests | |
| run: pnpm exec playwright test | |
| - name: Upload Playwright report | |
| uses: actions/upload-artifact@v7 | |
| if: failure() | |
| with: | |
| name: playwright-report | |
| path: playwright-report/ | |
| retention-days: 7 | |
| # ── Tier 3 only — Dockerfile security scan ────────────────────────────────── | |
| dockerfile-check: | |
| name: Dockerfile Check (Trivy) | |
| runs-on: ubuntu-latest | |
| needs: [detect, build] | |
| if: > | |
| needs.detect.outputs.has-lockfile == 'true' && | |
| needs.detect.outputs.has-vite == 'true' && | |
| needs.build.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Trivy Dockerfile scan | |
| uses: aquasecurity/trivy-action@v0.36.0 | |
| with: | |
| scan-type: config | |
| scan-ref: docker/Dockerfile.prod | |
| severity: CRITICAL,HIGH | |
| exit-code: "1" | |
| # ── Tier 2 + Tier 3 ───────────────────────────────────────────────────────── | |
| security: | |
| name: Security (pnpm audit) | |
| runs-on: ubuntu-latest | |
| needs: detect | |
| if: needs.detect.outputs.has-lockfile == 'true' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: "pnpm" | |
| - run: pnpm install --frozen-lockfile | |
| # Fail CI on high or critical vulnerabilities. | |
| # Do NOT use "|| true" — that makes this job permanently meaningless. | |
| - run: pnpm audit --audit-level=high | |
| # ── Gate ───────────────────────────────────────────────────────────────────── | |
| ci-passed: | |
| name: CI Passed ✅ | |
| runs-on: ubuntu-latest | |
| needs: [detect, typecheck, lint, test, build, e2e, dockerfile-check, security] | |
| if: always() | |
| steps: | |
| - name: Evaluate results | |
| run: | | |
| echo "──────────────────────────────────────────" | |
| echo " CI Gate — evaluating job results" | |
| echo "──────────────────────────────────────────" | |
| HAS_LOCKFILE="${{ needs.detect.outputs.has-lockfile }}" | |
| HAS_VITE="${{ needs.detect.outputs.has-vite }}" | |
| if [[ "$HAS_LOCKFILE" != "true" ]]; then | |
| echo "ℹ️ Tier 1: No project detected — all checks skipped" | |
| exit 0 | |
| fi | |
| if [[ "$HAS_VITE" != "true" ]]; then | |
| echo "ℹ️ Tier 2: Husky + commitlint only" | |
| echo " Active: security (pnpm audit)" | |
| echo " Skipped: typecheck, lint, test, build, e2e, dockerfile-check" | |
| else | |
| echo "✅ Tier 3: Full project" | |
| echo " All checks active" | |
| fi | |
| echo "" | |
| FAILED=0 | |
| check_job() { | |
| local name="$1" | |
| local result="$2" | |
| if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then | |
| echo "❌ $name: $result" | |
| FAILED=1 | |
| elif [[ "$result" == "skipped" ]]; then | |
| echo "⏭ $name: skipped (not applicable at current tier)" | |
| else | |
| echo "✅ $name: $result" | |
| fi | |
| } | |
| check_job "typecheck" "${{ needs.typecheck.result }}" | |
| check_job "lint" "${{ needs.lint.result }}" | |
| check_job "test" "${{ needs.test.result }}" | |
| check_job "build" "${{ needs.build.result }}" | |
| check_job "e2e" "${{ needs.e2e.result }}" | |
| check_job "dockerfile-check" "${{ needs.dockerfile-check.result }}" | |
| check_job "security" "${{ needs.security.result }}" | |
| echo "" | |
| if [[ "$FAILED" == "1" ]]; then | |
| echo "❌ CI gate FAILED — see failed jobs above" | |
| exit 1 | |
| fi | |
| echo "✅ CI gate PASSED" |