diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 039bdb0..aeaa17b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,6 @@ name: CI on: push: branches: [main, master] - pull_request: - branches: [main, master] jobs: build-and-test: diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..f1ee61e --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,123 @@ +name: PR Validation + +on: + pull_request: + branches: [master] + types: [opened, synchronize, reopened] + +concurrency: + group: pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + +jobs: + validate: + name: Validate Pull Request + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + id: install + run: npm ci + + - name: Type check + id: type-check + if: always() && steps.install.outcome == 'success' + run: npm run type-check + + - name: Lint + id: lint + if: always() && steps.install.outcome == 'success' + run: npm run lint + + - name: Format check + id: format + if: always() && steps.install.outcome == 'success' + run: npm run format:check + + - name: Test + id: test + if: always() && steps.install.outcome == 'success' + run: npm run test + + - name: Build + id: build + if: always() && steps.install.outcome == 'success' + run: npm run build + + - name: Comment on PR + if: always() + uses: actions/github-script@v8 + with: + script: | + const marker = ''; + const checks = [ + { name: 'Dependencies installed', outcome: '${{ steps.install.outcome }}' }, + { name: 'Type check passed', outcome: '${{ steps.type-check.outcome }}' }, + { name: 'Linting passed', outcome: '${{ steps.lint.outcome }}' }, + { name: 'Format check passed', outcome: '${{ steps.format.outcome }}' }, + { name: 'Tests passed', outcome: '${{ steps.test.outcome }}' }, + { name: 'Build successful', outcome: '${{ steps.build.outcome }}' }, + ]; + + const allPassed = checks.every(c => c.outcome === 'success'); + const status = allPassed ? '✅ PASSED' : '❌ FAILED'; + const emoji = allPassed ? '🎉' : '💥'; + + const checkLines = checks + .map(c => `- ${c.outcome === 'success' ? '✅' : c.outcome === 'skipped' ? '⏭️' : '❌'} ${c.name}`) + .join('\n'); + + const body = `${marker} + ${emoji} **PR Validation ${status}** + + **Commit:** \`${{ github.event.pull_request.head.sha }}\` + **Branch:** \`${{ github.head_ref }}\` + + **Checks:** + ${checkLines} + + ${allPassed ? '**Ready to merge!** ✨' : '**Please fix the failing checks.**'} + + --- + 🔗 [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + ⏰ Generated at: \`${new Date().toISOString()}\``; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => + c.user.login === 'github-actions[bot]' && c.body.startsWith(marker) + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body, + }); + }