Skip to content

feat: ✨ Add isEmpty helper #48

feat: ✨ Add isEmpty helper

feat: ✨ Add isEmpty helper #48

Workflow file for this run

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;