Skip to content

ci(deps): bump the github-actions-all group across 1 directory with 3… #13

ci(deps): bump the github-actions-all group across 1 directory with 3…

ci(deps): bump the github-actions-all group across 1 directory with 3… #13

Workflow file for this run

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"