feat: ✨ Add isEmpty helper #48
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: Pull Request Validation | |
| on: | |
| pull_request: | |
| branches: [main, develop] | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| checks: write | |
| statuses: write | |
| issues: write | |
| env: | |
| WORKFLOW_ID: ${{ github.run_id }} | |
| ARTIFACT_PACKAGE: package-json-${{ github.run_id }} | |
| ARTIFACT_BUILD: build-${{ github.run_id }} | |
| jobs: | |
| # Version calculation with artifact upload | |
| version-calculation: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| version-changed: ${{ steps.version.outputs.changed }} | |
| status: ${{ steps.version.outputs.status }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| - name: Enable corepack | |
| run: corepack enable | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Get current version | |
| id: version | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "status=success" >> $GITHUB_OUTPUT | |
| echo "changed=false" >> $GITHUB_OUTPUT | |
| # Build project with artifact upload | |
| build: | |
| needs: version-calculation | |
| uses: ./.github/workflows/job-build.yml | |
| with: | |
| node-version: "24" | |
| upload-artifact: true | |
| artifact-name: build-${{ github.run_id }} | |
| # Test job (runs in parallel with build) | |
| test: | |
| needs: version-calculation | |
| uses: ./.github/workflows/job-tests.yml | |
| with: | |
| node-version: "24" | |
| coverage: true | |
| lint: | |
| uses: ./.github/workflows/job-lint.yml | |
| with: | |
| node-version: "24" | |
| type-check: | |
| uses: ./.github/workflows/job-typecheck.yml | |
| with: | |
| node-version: "24" | |
| conventional-commits: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| status: ${{ steps.status.outputs.status }} | |
| steps: | |
| - name: Check conventional commits | |
| uses: helpers4/action/conventional-commits@main | |
| with: | |
| checkout: "true" | |
| pr-comment: "error" | |
| - name: Set status | |
| id: status | |
| if: always() | |
| run: | | |
| if [ ${{ job.status }} == 'success' ]; then | |
| echo "status=success" >> $GITHUB_OUTPUT | |
| else | |
| echo "status=failure" >> $GITHUB_OUTPUT | |
| fi | |
| # Verification job (depends on build for artifacts) | |
| verification: | |
| needs: [build, test] | |
| uses: ./.github/workflows/job-verification.yml | |
| with: | |
| node-version: "24" | |
| build-artifact: build-${{ github.run_id }} | |
| # PR comment with results | |
| pr-comment: | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| version-calculation, | |
| build, | |
| test, | |
| lint, | |
| type-check, | |
| conventional-commits, | |
| verification, | |
| ] | |
| if: always() | |
| steps: | |
| - name: Update PR comment | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const createJobsTable = (jobs) => { | |
| const rows = Object.entries(jobs).map(([job, status]) => { | |
| const icon = status === 'success' ? '✅' : | |
| status === 'failure' ? '❌' : | |
| status === 'skipped' ? '⏭️' : '⚠️'; | |
| const statusBadge = status === 'success' ? '`passing`' : | |
| status === 'failure' ? '`failing`' : | |
| status === 'skipped' ? '`skipped`' : '`unknown`'; | |
| return `| ${icon} | **${job}** | ${statusBadge} |`; | |
| }).join('\n'); | |
| return `| | Job | Status |\n|:---:|-----|:------:|\n${rows}`; | |
| }; | |
| const createCoverageSection = (coverage) => { | |
| const target = 100; | |
| const getStatus = (value) => { | |
| const num = parseFloat(value); | |
| if (num >= target) return { icon: '✅', color: 'brightgreen', label: 'perfect' }; | |
| if (num >= 90) return { icon: '🟢', color: 'green', label: 'excellent' }; | |
| if (num >= 80) return { icon: '🟡', color: 'yellow', label: 'good' }; | |
| if (num >= 60) return { icon: '🟠', color: 'orange', label: 'needs work' }; | |
| return { icon: '🔴', color: 'red', label: 'critical' }; | |
| }; | |
| const createProgressBar = (value) => { | |
| const pct = parseFloat(value); | |
| const filled = Math.floor(pct / 5); | |
| const empty = 20 - filled; | |
| return '`' + '▓'.repeat(filled) + '░'.repeat(empty) + '`'; | |
| }; | |
| const avgCoverage = (Object.values(coverage).reduce((a, b) => a + parseFloat(b), 0) / 4).toFixed(1); | |
| const avgStatus = getStatus(avgCoverage); | |
| const rows = Object.entries(coverage).map(([metric, value]) => { | |
| const status = getStatus(value); | |
| const bar = createProgressBar(value); | |
| return `| ${status.icon} | **${metric}** | ${bar} | **${value}%** |`; | |
| }).join('\n'); | |
| const header = [ | |
| `> ${avgStatus.icon} **Overall Coverage: ${avgCoverage}%** ${avgStatus.label === 'perfect' ? '— Target reached! 🎯' : '— Target: ' + target + '%'}`, | |
| '', | |
| '| | Metric | Progress | Coverage |', | |
| '|:---:|--------|:--------:|:--------:|', | |
| rows, | |
| ].join('\n'); | |
| return header; | |
| }; | |
| const jobs = { | |
| '🔢 Version': "${{ needs.version-calculation.outputs.status || 'unknown' }}", | |
| '🏗️ Build': "${{ needs.build.outputs.status || 'unknown' }}", | |
| '🧪 Tests': "${{ needs.test.outputs.status || 'unknown' }}", | |
| '📝 Lint': "${{ needs.lint.outputs.status || 'unknown' }}", | |
| '📘 TypeCheck': "${{ needs.type-check.outputs.status || 'unknown' }}", | |
| '� Conventional Commits': "${{ needs.conventional-commits.outputs.status || 'unknown' }}", | |
| '�🔗 Coherency': "${{ needs.verification.outputs.status || 'unknown' }}", | |
| }; | |
| const coverage = { | |
| 'Lines': "${{ needs.test.outputs.coverage-lines || '0' }}", | |
| 'Branches': "${{ needs.test.outputs.coverage-branches || '0' }}", | |
| 'Functions': "${{ needs.test.outputs.coverage-functions || '0' }}", | |
| 'Statements': "${{ needs.test.outputs.coverage-statements || '0' }}", | |
| }; | |
| const passedCount = Object.values(jobs).filter(s => s === 'success').length; | |
| const totalJobs = Object.keys(jobs).length; | |
| const allPassed = passedCount === totalJobs; | |
| const avgCoverage = (Object.values(coverage).reduce((a, b) => a + parseFloat(b), 0) / 4).toFixed(1); | |
| const coverageOk = parseFloat(avgCoverage) >= 100; | |
| const statusBanner = allPassed && coverageOk | |
| ? '## ✅ PR Validation Passed\n\n> All checks passed and coverage target reached!' | |
| : allPassed | |
| ? `## 🟡 PR Validation Passed\n\n> All checks passed • Coverage: ${avgCoverage}% (target: 100%)` | |
| : `## ❌ PR Validation Failed\n\n> ${passedCount}/${totalJobs} checks passed`; | |
| const comment = `${statusBanner} | |
| --- | |
| ### 📋 Pipeline Status | |
| ${createJobsTable(jobs)} | |
| --- | |
| ### 📊 Code Coverage | |
| ${createCoverageSection(coverage)} | |
| --- | |
| <details> | |
| <summary>ℹ️ <b>About this report</b></summary> | |
| - 🎯 **Coverage Target**: 100% for all metrics | |
| - 🔄 This comment updates automatically with each push | |
| - 📈 Coverage is measured using Vitest + v8 | |
| </details> | |
| <sub>🤖 Generated by **@helpers4** CI • ${new Date().toISOString().split('T')[0]}</sub>`; | |
| try { | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && comment.body.includes('PR Validation') | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: comment | |
| }); | |
| console.log('Updated existing comment'); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: comment | |
| }); | |
| console.log('Created new comment'); | |
| } | |
| } catch (error) { | |
| console.error('Error updating PR comment:', error); | |
| console.log('Comment update failed, but continuing workflow...'); | |
| } | |
| return true; |