Skip to content

Fix CI heredoc errors, add Visual/UX reviewers & composite actions #19

Fix CI heredoc errors, add Visual/UX reviewers & composite actions

Fix CI heredoc errors, add Visual/UX reviewers & composite actions #19

Workflow file for this run

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 }}