Fix CI heredoc errors, add Visual/UX reviewers & composite actions #19
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: | |
| pull_request: | |
| branches: ['**'] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: read | |
| concurrency: | |
| group: ci-${{ github.head_ref || github.ref_name }} | |
| cancel-in-progress: true | |
| env: | |
| NODE_VERSION: '22' | |
| jobs: | |
| # ============================================ | |
| # STATIC ANALYSIS | |
| # ============================================ | |
| lint: | |
| name: "Static Analysis / Lint (ESLint)" | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has_app: ${{ steps.detect.outputs.has_app }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Detect environment | |
| id: detect | |
| run: | | |
| # Detect package manager | |
| PM="npm" | |
| [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" | |
| [ -f "pnpm-lock.yaml" ] && PM="pnpm" | |
| [ -f "yarn.lock" ] && PM="yarn" | |
| echo "pm=$PM" >> $GITHUB_OUTPUT | |
| # Detect app | |
| HAS_APP=false | |
| [ -f "next.config.js" ] && HAS_APP=true | |
| [ -f "next.config.mjs" ] && HAS_APP=true | |
| [ -f "next.config.ts" ] && HAS_APP=true | |
| [ -f "vite.config.ts" ] && HAS_APP=true | |
| [ -f "vite.config.js" ] && HAS_APP=true | |
| [ -f "playwright.config.ts" ] && HAS_APP=true | |
| ls apps/*/package.json >/dev/null 2>&1 && HAS_APP=true | |
| [ -d "src/app" ] && HAS_APP=true | |
| echo "has_app=$HAS_APP" >> $GITHUB_OUTPUT | |
| - uses: actions/setup-node@v4 | |
| if: steps.detect.outputs.pm != 'bun' | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - uses: oven-sh/setup-bun@v2 | |
| if: steps.detect.outputs.pm == 'bun' | |
| - name: Install dependencies | |
| run: | | |
| case "${{ steps.detect.outputs.pm }}" in | |
| bun) bun install --frozen-lockfile ;; | |
| pnpm) corepack enable && pnpm install --frozen-lockfile ;; | |
| yarn) yarn --frozen-lockfile ;; | |
| *) npm ci ;; | |
| esac | |
| - name: Lint | |
| run: | | |
| PM="${{ steps.detect.outputs.pm }}" | |
| $PM run lint | |
| types: | |
| name: "Static Analysis / Types (TypeScript)" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Detect package manager | |
| id: detect | |
| run: | | |
| PM="npm" | |
| [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" | |
| [ -f "pnpm-lock.yaml" ] && PM="pnpm" | |
| [ -f "yarn.lock" ] && PM="yarn" | |
| echo "pm=$PM" >> $GITHUB_OUTPUT | |
| - uses: actions/setup-node@v4 | |
| if: steps.detect.outputs.pm != 'bun' | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - uses: oven-sh/setup-bun@v2 | |
| if: steps.detect.outputs.pm == 'bun' | |
| - name: Install dependencies | |
| run: | | |
| case "${{ steps.detect.outputs.pm }}" in | |
| bun) bun install --frozen-lockfile ;; | |
| pnpm) corepack enable && pnpm install --frozen-lockfile ;; | |
| yarn) yarn --frozen-lockfile ;; | |
| *) npm ci ;; | |
| esac | |
| - name: Typecheck | |
| run: | | |
| PM="${{ steps.detect.outputs.pm }}" | |
| $PM run typecheck || npx tsc --noEmit | |
| # ============================================ | |
| # TESTS (requires Static Analysis) | |
| # ============================================ | |
| unit: | |
| name: "Tests / Unit (Vitest)" | |
| needs: [lint, types] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Detect package manager | |
| id: detect | |
| run: | | |
| PM="npm" | |
| [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" | |
| [ -f "pnpm-lock.yaml" ] && PM="pnpm" | |
| [ -f "yarn.lock" ] && PM="yarn" | |
| echo "pm=$PM" >> $GITHUB_OUTPUT | |
| - uses: actions/setup-node@v4 | |
| if: steps.detect.outputs.pm != 'bun' | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - uses: oven-sh/setup-bun@v2 | |
| if: steps.detect.outputs.pm == 'bun' | |
| - name: Install dependencies | |
| run: | | |
| case "${{ steps.detect.outputs.pm }}" in | |
| bun) bun install --frozen-lockfile ;; | |
| pnpm) corepack enable && pnpm install --frozen-lockfile ;; | |
| yarn) yarn --frozen-lockfile ;; | |
| *) npm ci ;; | |
| esac | |
| - name: Run unit tests | |
| run: | | |
| PM="${{ steps.detect.outputs.pm }}" | |
| if [ -f "vitest.config.ts" ] || [ -f "vitest.config.js" ]; then | |
| $PM run test -- --passWithNoTests || npx vitest run --passWithNoTests | |
| else | |
| echo "No vitest config found, skipping" | |
| fi | |
| e2e: | |
| name: "Tests / E2E (Playwright)" | |
| needs: [lint, unit] | |
| if: needs.lint.outputs.has_app == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Detect package manager | |
| id: detect | |
| run: | | |
| PM="npm" | |
| [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" | |
| [ -f "pnpm-lock.yaml" ] && PM="pnpm" | |
| [ -f "yarn.lock" ] && PM="yarn" | |
| echo "pm=$PM" >> $GITHUB_OUTPUT | |
| - uses: actions/setup-node@v4 | |
| if: steps.detect.outputs.pm != 'bun' | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - uses: oven-sh/setup-bun@v2 | |
| if: steps.detect.outputs.pm == 'bun' | |
| - name: Install dependencies | |
| run: | | |
| case "${{ steps.detect.outputs.pm }}" in | |
| bun) bun install --frozen-lockfile ;; | |
| pnpm) corepack enable && pnpm install --frozen-lockfile ;; | |
| yarn) yarn --frozen-lockfile ;; | |
| *) npm ci ;; | |
| esac | |
| - run: npx playwright install chromium --with-deps | |
| - name: Run E2E tests | |
| run: | | |
| mkdir -p .claude/screenshots | |
| npx playwright test --project=chromium | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: playwright-screenshots-${{ github.sha }} | |
| path: .claude/screenshots/ | |
| retention-days: 7 | |
| # ============================================ | |
| # REVIEWS (requires Tests, PR only) | |
| # ============================================ | |
| # Uses composite actions from .github/actions/ for cleaner workflow | |
| # Each reviewer action handles: context gathering, Claude review, result extraction | |
| # The review-comment action handles: updating the consolidated PR comment | |
| requirements: | |
| name: "Reviews / Requirements" | |
| needs: [unit, e2e] | |
| if: github.event_name == 'pull_request' && !failure() && !cancelled() | |
| runs-on: ubuntu-latest | |
| outputs: | |
| passed: ${{ steps.review.outputs.passed }} | |
| result: ${{ steps.review.outputs.result }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Run requirements review | |
| id: review | |
| uses: ./.github/actions/requirements-reviewer | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| branch: ${{ github.head_ref }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update comment | |
| if: always() | |
| uses: ./.github/actions/review-comment | |
| with: | |
| review_name: Requirements | |
| passed: ${{ steps.review.outputs.passed }} | |
| result_json: ${{ steps.review.outputs.result }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| sha: ${{ github.sha }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| code-quality: | |
| name: "Reviews / Code Quality" | |
| needs: [requirements] | |
| if: github.event_name == 'pull_request' && needs.requirements.result == 'success' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| passed: ${{ steps.review.outputs.passed }} | |
| result: ${{ steps.review.outputs.result }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Run code quality review | |
| id: review | |
| uses: ./.github/actions/code-quality-reviewer | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update comment | |
| if: always() | |
| uses: ./.github/actions/review-comment | |
| with: | |
| review_name: Code Quality | |
| passed: ${{ steps.review.outputs.passed }} | |
| result_json: ${{ steps.review.outputs.result }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| sha: ${{ github.sha }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| context: | |
| name: "Reviews / Context" | |
| needs: [requirements] | |
| if: github.event_name == 'pull_request' && needs.requirements.result == 'success' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| passed: ${{ steps.review.outputs.passed }} | |
| result: ${{ steps.review.outputs.result }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Run context review | |
| id: review | |
| uses: ./.github/actions/context-reviewer | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update comment | |
| if: always() | |
| uses: ./.github/actions/review-comment | |
| with: | |
| review_name: Context | |
| passed: ${{ steps.review.outputs.passed }} | |
| result_json: ${{ steps.review.outputs.result }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| sha: ${{ github.sha }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| visual: | |
| name: "Reviews / Visual" | |
| needs: [lint, requirements, e2e] | |
| if: github.event_name == 'pull_request' && needs.lint.outputs.has_app == 'true' && needs.requirements.result == 'success' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| passed: ${{ steps.review.outputs.passed }} | |
| result: ${{ steps.review.outputs.result }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: playwright-screenshots-${{ github.sha }} | |
| path: .claude/screenshots/ | |
| continue-on-error: true | |
| - name: Run visual review | |
| id: review | |
| uses: ./.github/actions/visual-reviewer | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update comment | |
| if: always() | |
| uses: ./.github/actions/review-comment | |
| with: | |
| review_name: Visual | |
| passed: ${{ steps.review.outputs.passed }} | |
| result_json: ${{ steps.review.outputs.result }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| sha: ${{ github.sha }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| ux: | |
| name: "Reviews / UX" | |
| needs: [lint, requirements, e2e] | |
| if: github.event_name == 'pull_request' && needs.lint.outputs.has_app == 'true' && needs.requirements.result == 'success' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| passed: ${{ steps.review.outputs.passed }} | |
| result: ${{ steps.review.outputs.result }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Run UX review | |
| id: review | |
| uses: ./.github/actions/ux-reviewer | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update comment | |
| if: always() | |
| uses: ./.github/actions/review-comment | |
| with: | |
| review_name: UX | |
| passed: ${{ steps.review.outputs.passed }} | |
| result_json: ${{ steps.review.outputs.result }} | |
| pr_number: ${{ github.event.pull_request.number }} | |
| sha: ${{ github.sha }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} |