chore(deps-dev): bump semantic-release from 24.2.9 to 25.0.1 #195
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: | |
| push: | |
| branches: | |
| - main | |
| - 'feat/**' | |
| - 'fix/**' | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| branches: | |
| - main | |
| # Cancel in-progress runs when a new workflow with the same group name is triggered | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| checks: write | |
| statuses: write | |
| jobs: | |
| test: | |
| name: Unit Tests & Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for better coverage comparison | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run unit tests with coverage | |
| run: npm test | |
| - name: Upload coverage reports to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage/lcov.info | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: true # Fail CI if coverage upload fails | |
| verbose: true | |
| coverage-check: | |
| name: Coverage Protection Check | |
| runs-on: ubuntu-latest | |
| needs: test | |
| # Run on pull_request events and on pushes to branches with open PRs | |
| if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref != 'refs/heads/main') | |
| steps: | |
| - name: Checkout PR code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get PR number | |
| id: pr | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| let prNumber; | |
| if (context.eventName === 'pull_request') { | |
| prNumber = context.issue.number; | |
| } else { | |
| // For push events, find PR by branch | |
| const { data: pulls } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}` | |
| }); | |
| if (pulls.length > 0) { | |
| prNumber = pulls[0].number; | |
| } else { | |
| core.setFailed('No open PR found for this branch'); | |
| return; | |
| } | |
| } | |
| core.setOutput('number', prNumber); | |
| core.exportVariable('PR_NUMBER', prNumber); | |
| console.log(`PR Number: ${prNumber}`); | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run tests with coverage on PR branch | |
| run: npm test | |
| continue-on-error: true | |
| - name: Save PR coverage | |
| run: | | |
| if [ -f coverage/coverage-summary.json ]; then | |
| cp coverage/coverage-summary.json coverage-pr.json | |
| else | |
| echo "⚠️ Coverage summary not found for PR branch" | |
| exit 1 | |
| fi | |
| - name: Checkout main branch | |
| run: | | |
| git fetch origin main | |
| git checkout main | |
| - name: Install dependencies on main | |
| run: npm ci | |
| - name: Run tests with coverage on main branch | |
| run: npm test | |
| continue-on-error: true | |
| - name: Save main coverage | |
| run: | | |
| if [ -f coverage/coverage-summary.json ]; then | |
| cp coverage/coverage-summary.json coverage-main.json | |
| echo "✅ Main branch coverage summary found" | |
| echo "MAIN_BRANCH_NO_COVERAGE=false" >> "$GITHUB_ENV" | |
| else | |
| echo "⚠️ Coverage summary not found for main branch" | |
| echo "This is expected if main branch doesn't have json-summary reporter yet." | |
| echo "Creating a fallback coverage file with 0% coverage to allow the check to pass." | |
| # Create a minimal coverage file with 0% to ensure PR doesn't fail | |
| echo '{"total":{"lines":{"total":0,"covered":0,"skipped":0,"pct":0},"statements":{"total":0,"covered":0,"skipped":0,"pct":0},"functions":{"total":0,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":0}}}' > coverage-main.json | |
| echo "MAIN_BRANCH_NO_COVERAGE=true" >> "$GITHUB_ENV" | |
| fi | |
| - name: Compare coverage and fail if decreased | |
| run: | | |
| echo "📊 Comparing coverage between main and PR..." | |
| # Extract coverage percentages from main branch using jq | |
| MAIN_LINES=$(jq -r '.total.lines.pct // 0' coverage-main.json) | |
| MAIN_STATEMENTS=$(jq -r '.total.statements.pct // 0' coverage-main.json) | |
| MAIN_FUNCTIONS=$(jq -r '.total.functions.pct // 0' coverage-main.json) | |
| MAIN_BRANCHES=$(jq -r '.total.branches.pct // 0' coverage-main.json) | |
| # Extract coverage percentages from PR branch using jq | |
| PR_LINES=$(jq -r '.total.lines.pct // 0' coverage-pr.json) | |
| PR_STATEMENTS=$(jq -r '.total.statements.pct // 0' coverage-pr.json) | |
| PR_FUNCTIONS=$(jq -r '.total.functions.pct // 0' coverage-pr.json) | |
| PR_BRANCHES=$(jq -r '.total.branches.pct // 0' coverage-pr.json) | |
| echo "Main branch coverage:" | |
| echo " Lines: ${MAIN_LINES}%" | |
| echo " Statements: ${MAIN_STATEMENTS}%" | |
| echo " Functions: ${MAIN_FUNCTIONS}%" | |
| echo " Branches: ${MAIN_BRANCHES}%" | |
| echo "" | |
| echo "PR branch coverage:" | |
| echo " Lines: ${PR_LINES}%" | |
| echo " Statements: ${PR_STATEMENTS}%" | |
| echo " Functions: ${PR_FUNCTIONS}%" | |
| echo " Branches: ${PR_BRANCHES}%" | |
| echo "" | |
| # Compare coverage (using bc for floating point comparison) | |
| # Note: 0% change is acceptable (no decrease) | |
| FAILED=0 | |
| # Use bc -l for comparison, result is 1 if true, 0 if false | |
| if [ $(echo "$PR_LINES < $MAIN_LINES" | bc -l) -eq 1 ]; then | |
| echo "❌ Lines coverage decreased from ${MAIN_LINES}% to ${PR_LINES}%" | |
| FAILED=1 | |
| else | |
| echo "✅ Lines coverage: ${PR_LINES}% (main: ${MAIN_LINES}%)" | |
| fi | |
| if [ $(echo "$PR_STATEMENTS < $MAIN_STATEMENTS" | bc -l) -eq 1 ]; then | |
| echo "❌ Statements coverage decreased from ${MAIN_STATEMENTS}% to ${PR_STATEMENTS}%" | |
| FAILED=1 | |
| else | |
| echo "✅ Statements coverage: ${PR_STATEMENTS}% (main: ${MAIN_STATEMENTS}%)" | |
| fi | |
| if [ $(echo "$PR_FUNCTIONS < $MAIN_FUNCTIONS" | bc -l) -eq 1 ]; then | |
| echo "❌ Functions coverage decreased from ${MAIN_FUNCTIONS}% to ${MAIN_FUNCTIONS}%" | |
| FAILED=1 | |
| else | |
| echo "✅ Functions coverage: ${PR_FUNCTIONS}% (main: ${MAIN_FUNCTIONS}%)" | |
| fi | |
| if [ $(echo "$PR_BRANCHES < $MAIN_BRANCHES" | bc -l) -eq 1 ]; then | |
| echo "❌ Branches coverage decreased from ${MAIN_BRANCHES}% to ${PR_BRANCHES}%" | |
| FAILED=1 | |
| else | |
| echo "✅ Branches coverage: ${PR_BRANCHES}% (main: ${MAIN_BRANCHES}%)" | |
| fi | |
| if [ $FAILED -eq 1 ]; then | |
| echo "" | |
| echo "❌ Coverage check FAILED: Coverage has decreased compared to main branch" | |
| echo "Please add tests to maintain or improve code coverage." | |
| echo "COVERAGE_STATUS=failed" >> "$GITHUB_ENV" | |
| echo "COVERAGE_FAILED=1" >> "$GITHUB_ENV" | |
| else | |
| echo "" | |
| echo "✅ Coverage check PASSED: Coverage maintained or improved" | |
| echo "COVERAGE_STATUS=passed" >> "$GITHUB_ENV" | |
| echo "COVERAGE_FAILED=0" >> "$GITHUB_ENV" | |
| fi | |
| # Save coverage values for PR comment | |
| echo "MAIN_LINES=${MAIN_LINES}" >> "$GITHUB_ENV" | |
| echo "MAIN_STATEMENTS=${MAIN_STATEMENTS}" >> "$GITHUB_ENV" | |
| echo "MAIN_FUNCTIONS=${MAIN_FUNCTIONS}" >> "$GITHUB_ENV" | |
| echo "MAIN_BRANCHES=${MAIN_BRANCHES}" >> "$GITHUB_ENV" | |
| echo "PR_LINES=${PR_LINES}" >> "$GITHUB_ENV" | |
| echo "PR_STATEMENTS=${PR_STATEMENTS}" >> "$GITHUB_ENV" | |
| echo "PR_FUNCTIONS=${PR_FUNCTIONS}" >> "$GITHUB_ENV" | |
| echo "PR_BRANCHES=${PR_BRANCHES}" >> "$GITHUB_ENV" | |
| # Calculate coverage diff for display | |
| LINES_DIFF=$(echo "$PR_LINES - $MAIN_LINES" | bc -l) | |
| echo "COVERAGE_DIFF=${LINES_DIFF}" >> "$GITHUB_ENV" | |
| - name: Create coverage status check | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prNumber = parseInt(process.env.PR_NUMBER); | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const mainLines = parseFloat(process.env.MAIN_LINES || '0'); | |
| const prLines = parseFloat(process.env.PR_LINES || '0'); | |
| const diff = prLines - mainLines; | |
| const failed = process.env.COVERAGE_FAILED === '1'; | |
| const mainBranchNoCoverage = process.env.MAIN_BRANCH_NO_COVERAGE === 'true'; | |
| // Format diff with sign | |
| const sign = diff > 0 ? '+' : ''; | |
| const diffStr = `${sign}${diff.toFixed(2)}%`; | |
| // Determine emoji and conclusion | |
| let emoji = '➡️'; | |
| let conclusion = 'success'; | |
| if (diff > 0) { | |
| emoji = '📈'; | |
| conclusion = 'success'; | |
| } else if (diff < 0) { | |
| emoji = '📉'; | |
| conclusion = failed ? 'failure' : 'neutral'; | |
| } | |
| // Create title based on scenario | |
| let title, summary; | |
| if (mainBranchNoCoverage) { | |
| title = `Coverage: ${prLines.toFixed(2)}% (First PR)`; | |
| summary = `**Initial Coverage:** ${prLines.toFixed(2)}%\n\nThis is the first PR with coverage protection. Future PRs will show coverage diff.`; | |
| } else { | |
| title = `Coverage: ${diffStr} ${emoji}`; | |
| summary = `**Coverage Change:** ${diffStr}\n**Main Branch:** ${mainLines.toFixed(2)}%\n**This PR:** ${prLines.toFixed(2)}%`; | |
| } | |
| // Create check run with coverage diff in title | |
| await github.rest.checks.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: title, | |
| head_sha: pr.head.sha, | |
| status: 'completed', | |
| conclusion: conclusion, | |
| output: { | |
| title: title, | |
| summary: summary, | |
| text: failed | |
| ? '⚠️ Coverage decreased. Please add tests to maintain or improve coverage.' | |
| : '✅ Coverage maintained or improved.' | |
| } | |
| }); | |
| // Also create a commit status for additional visibility | |
| const state = failed ? 'failure' : 'success'; | |
| const description = mainBranchNoCoverage | |
| ? `Coverage: ${prLines.toFixed(2)}% (initial)` | |
| : `Coverage: ${diffStr} (${mainLines.toFixed(2)}% → ${prLines.toFixed(2)}%)`; | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: pr.head.sha, | |
| state: state, | |
| context: 'Coverage Change', | |
| description: description, | |
| target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}` | |
| }); | |
| - name: Generate per-file coverage diff | |
| id: coverage-diff | |
| run: | | |
| echo "Generating per-file coverage diff..." | |
| # Create coverage diff report using heredoc to avoid shell interpolation | |
| node << 'EOF' | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| // Read coverage files | |
| const mainCoverage = JSON.parse(fs.readFileSync('coverage-main.json', 'utf8')); | |
| const prCoverage = JSON.parse(fs.readFileSync('coverage-pr.json', 'utf8')); | |
| const annotations = []; | |
| const diffReport = []; | |
| // Compare per-file coverage | |
| for (const [filePath, prData] of Object.entries(prCoverage)) { | |
| if (filePath === 'total') continue; | |
| const mainData = mainCoverage[filePath]; | |
| if (!mainData) { | |
| // New file - no comparison needed | |
| continue; | |
| } | |
| const prLines = prData.lines.pct; | |
| const mainLines = mainData.lines.pct; | |
| const diff = prLines - mainLines; | |
| if (diff < 0) { | |
| // Coverage decreased for this file | |
| const relativePath = filePath.replace(process.cwd() + '/', ''); | |
| annotations.push({ | |
| path: relativePath, | |
| start_line: 1, | |
| end_line: 1, | |
| annotation_level: 'warning', | |
| message: `Coverage decreased by ${Math.abs(diff).toFixed(2)}% (from ${mainLines}% to ${prLines}%)`, | |
| title: 'Coverage Decreased' | |
| }); | |
| diffReport.push({ | |
| file: relativePath, | |
| main: mainLines, | |
| pr: prLines, | |
| diff: diff | |
| }); | |
| } else if (diff > 0) { | |
| const relativePath = filePath.replace(process.cwd() + '/', ''); | |
| diffReport.push({ | |
| file: relativePath, | |
| main: mainLines, | |
| pr: prLines, | |
| diff: diff | |
| }); | |
| } | |
| } | |
| // Save annotations and diff report | |
| fs.writeFileSync('coverage-annotations.json', JSON.stringify(annotations, null, 2)); | |
| fs.writeFileSync('coverage-diff-report.json', JSON.stringify(diffReport, null, 2)); | |
| console.log(`Generated ${annotations.length} annotations for files with decreased coverage`); | |
| console.log(`Total files with coverage changes: ${diffReport.length}`); | |
| EOF | |
| - name: Create coverage annotations | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| // Read annotations | |
| let annotations = []; | |
| try { | |
| annotations = JSON.parse(fs.readFileSync('coverage-annotations.json', 'utf8')); | |
| } catch (e) { | |
| console.log('No annotations file found'); | |
| return; | |
| } | |
| if (annotations.length === 0) { | |
| console.log('No coverage annotations to create'); | |
| return; | |
| } | |
| // Create check run with annotations | |
| const prNumber = parseInt(process.env.PR_NUMBER); | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| await github.rest.checks.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: 'Per-File Coverage Changes', | |
| head_sha: pr.head.sha, | |
| status: 'completed', | |
| conclusion: annotations.length > 0 ? 'neutral' : 'success', | |
| output: { | |
| title: 'Coverage Changes by File', | |
| summary: `Found ${annotations.length} file(s) with decreased coverage`, | |
| annotations: annotations.slice(0, 50) // GitHub limits to 50 annotations per request | |
| } | |
| }); | |
| console.log(`Created check run with ${annotations.length} annotations`); | |
| - name: Comment PR with coverage comparison | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const status = process.env.COVERAGE_STATUS; | |
| const failed = process.env.COVERAGE_FAILED === '1'; | |
| const mainBranchNoCoverage = process.env.MAIN_BRANCH_NO_COVERAGE === 'true'; | |
| const mainLines = process.env.MAIN_LINES || '0'; | |
| const mainStatements = process.env.MAIN_STATEMENTS || '0'; | |
| const mainFunctions = process.env.MAIN_FUNCTIONS || '0'; | |
| const mainBranches = process.env.MAIN_BRANCHES || '0'; | |
| const prLines = process.env.PR_LINES || '0'; | |
| const prStatements = process.env.PR_STATEMENTS || '0'; | |
| const prFunctions = process.env.PR_FUNCTIONS || '0'; | |
| const prBranches = process.env.PR_BRANCHES || '0'; | |
| const getIcon = (pr, main) => { | |
| const prNum = parseFloat(pr); | |
| const mainNum = parseFloat(main); | |
| if (prNum > mainNum) return '📈'; | |
| if (prNum < mainNum) return '📉'; | |
| return '➡️'; | |
| }; | |
| const getStatus = (pr, main) => { | |
| const prNum = parseFloat(pr); | |
| const mainNum = parseFloat(main); | |
| if (prNum < mainNum) return '❌'; | |
| return '✅'; | |
| }; | |
| const statusIcon = failed ? '❌' : '✅'; | |
| const statusText = failed ? 'FAILED - Coverage Decreased' : 'PASSED - Coverage Maintained'; | |
| let body; | |
| if (mainBranchNoCoverage) { | |
| // Special message for first-time setup | |
| body = `## ℹ️ Code Coverage Protection - First Time Setup | |
| **Status:** ✅ PASSED (Initial Setup) | |
| ### PR Coverage | |
| | Metric | This PR | | |
| |--------|---------| | |
| | Lines | ${prLines}% | | |
| | Statements | ${prStatements}% | | |
| | Functions | ${prFunctions}% | | |
| | Branches | ${prBranches}% | | |
| ### 📝 Note | |
| This is the first PR with coverage protection enabled. The main branch doesn't have the \`json-summary\` reporter configured yet, so we can't compare coverage. | |
| **Once this PR is merged**, all future PRs will be compared against the main branch coverage and will be blocked if coverage decreases. | |
| --- | |
| *Coverage protection will be fully active after this PR is merged.*`; | |
| } else { | |
| // Normal coverage comparison | |
| // Read per-file diff report | |
| const fs = require('fs'); | |
| let fileDiffSection = ''; | |
| try { | |
| const diffReport = JSON.parse(fs.readFileSync('coverage-diff-report.json', 'utf8')); | |
| if (diffReport.length > 0) { | |
| // Sort by diff (worst first) | |
| diffReport.sort((a, b) => a.diff - b.diff); | |
| // Take top 10 files with biggest changes | |
| const topFiles = diffReport.slice(0, 10); | |
| const fileRows = topFiles.map(f => { | |
| const icon = f.diff > 0 ? '📈' : f.diff < 0 ? '📉' : '➡️'; | |
| const sign = f.diff > 0 ? '+' : ''; | |
| return `| ${f.file} | ${f.main.toFixed(2)}% | ${f.pr.toFixed(2)}% | ${icon} ${sign}${f.diff.toFixed(2)}% |`; | |
| }).join('\n '); | |
| const showingText = diffReport.length > 10 ? `\n *Showing top 10 of ${diffReport.length} files with coverage changes*` : ''; | |
| fileDiffSection = ` | |
| ### 📊 Coverage Changes by File | |
| | File | Main | This PR | Change | | |
| |------|------|---------|--------| | |
| ${fileRows}${showingText} | |
| `; | |
| } | |
| } catch (e) { | |
| console.log('No per-file diff report found'); | |
| } | |
| body = `## ${statusIcon} Code Coverage Check | |
| **Status:** ${statusText} | |
| ### Coverage Comparison | |
| | Metric | Main Branch | This PR | Change | Status | | |
| |--------|-------------|---------|--------|--------| | |
| | Lines | ${mainLines}% | ${prLines}% | ${getIcon(prLines, mainLines)} ${(parseFloat(prLines) - parseFloat(mainLines)).toFixed(2)}% | ${getStatus(prLines, mainLines)} | | |
| | Statements | ${mainStatements}% | ${prStatements}% | ${getIcon(prStatements, mainStatements)} ${(parseFloat(prStatements) - parseFloat(mainStatements)).toFixed(2)}% | ${getStatus(prStatements, mainStatements)} | | |
| | Functions | ${mainFunctions}% | ${prFunctions}% | ${getIcon(prFunctions, mainFunctions)} ${(parseFloat(prFunctions) - parseFloat(mainFunctions)).toFixed(2)}% | ${getStatus(prFunctions, mainFunctions)} | | |
| | Branches | ${mainBranches}% | ${prBranches}% | ${getIcon(prBranches, mainBranches)} ${(parseFloat(prBranches) - parseFloat(mainBranches)).toFixed(2)}% | ${getStatus(prBranches, mainBranches)} | | |
| ${fileDiffSection} | |
| ${failed | |
| ? '### ⚠️ Action Required\\n\\nThis PR decreases code coverage. Please add tests to cover the new/modified code before merging.\\n\\n**This check is blocking the PR from being merged.**' | |
| : '### ✅ Great Job!\\n\\nCode coverage has been maintained or improved. This PR is ready for review.'} | |
| --- | |
| *Coverage protection is enabled. PRs that decrease coverage will be blocked from merging.*`; | |
| } | |
| // Find existing coverage comment | |
| const prNumber = parseInt(process.env.PR_NUMBER); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: prNumber, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('Code Coverage Check') | |
| ); | |
| if (botComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| comment_id: botComment.id, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| issue_number: prNumber, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| } | |
| - name: Fail if coverage decreased | |
| if: env.COVERAGE_FAILED == '1' | |
| run: | | |
| echo "❌ Coverage check failed - blocking PR merge" | |
| exit 1 | |
| esm-validation: | |
| name: ESM Build Validation | |
| runs-on: ubuntu-latest | |
| needs: test # Run after unit tests pass | |
| strategy: | |
| matrix: | |
| node-version: [18, 20, 22] # Test on multiple Node.js versions | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build project (CJS + ESM) | |
| run: npm run build | |
| - name: Verify ESM build artifacts exist | |
| run: | | |
| echo "Checking ESM build artifacts..." | |
| test -d build/esm || (echo "❌ ESM build directory not found" && exit 1) | |
| test -f build/esm/index.js || (echo "❌ ESM entry point not found" && exit 1) | |
| test -f build/esm/package.json || (echo "❌ ESM package.json marker not found" && exit 1) | |
| echo "✅ ESM build artifacts verified" | |
| - name: Verify ESM package.json type marker | |
| run: | | |
| echo "Checking ESM package.json marker..." | |
| grep -q '"type".*"module"' build/esm/package.json || (echo "❌ ESM package.json missing type:module" && exit 1) | |
| echo "✅ ESM package.json type marker verified" | |
| - name: Run ESM integration tests | |
| run: npm run test:esm | |
| - name: Test ESM import in Node.js (smoke test) | |
| run: | | |
| echo "Testing direct ESM import..." | |
| node -e "import('./build/esm/index.js').then(m => { console.log('✅ ESM import successful'); console.log('Exports:', Object.keys(m).join(', ')); }).catch(e => { console.error('❌ ESM import failed:', e.message); process.exit(1); })" | |
| - name: Upload ESM test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: esm-test-results-node-${{ matrix.node-version }} | |
| path: | | |
| build/esm/**/*.js | |
| build/esm/package.json | |
| retention-days: 7 | |
| esm-validation-summary: | |
| name: ESM Validation Summary | |
| runs-on: ubuntu-latest | |
| needs: esm-validation | |
| # Run on pull_request events and on pushes to branches with open PRs | |
| if: always() && (github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref != 'refs/heads/main')) | |
| steps: | |
| - name: Get PR number | |
| id: pr | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| let prNumber; | |
| if (context.eventName === 'pull_request') { | |
| prNumber = context.issue.number; | |
| } else { | |
| // For push events, find PR by branch | |
| const { data: pulls } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| head: `${context.repo.owner}:${context.ref.replace('refs/heads/', '')}` | |
| }); | |
| if (pulls.length > 0) { | |
| prNumber = pulls[0].number; | |
| } else { | |
| core.setFailed('No open PR found for this branch'); | |
| return; | |
| } | |
| } | |
| core.setOutput('number', prNumber); | |
| core.exportVariable('PR_NUMBER', prNumber); | |
| console.log(`PR Number: ${prNumber}`); | |
| - name: Comment PR with ESM validation results | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const conclusion = '${{ needs.esm-validation.result }}'; | |
| const nodeVersions = ['18', '20', '22']; | |
| let statusIcon = conclusion === 'success' ? '✅' : '❌'; | |
| let statusText = conclusion === 'success' ? 'All ESM validation checks passed!' : 'ESM validation failed!'; | |
| const body = `## ${statusIcon} ESM Build Validation | |
| **Status:** ${statusText} | |
| ### Test Matrix Results | |
| | Node.js Version | Status | | |
| |-----------------|--------| | |
| ${nodeVersions.map(v => `| ${v} | ${conclusion === 'success' ? '✅ Passed' : '❌ Failed'} |`).join('\n')} | |
| ### Validation Steps | |
| - ${conclusion === 'success' ? '✅' : '❌'} ESM build artifacts generated | |
| - ${conclusion === 'success' ? '✅' : '❌'} \`package.json\` type marker present | |
| - ${conclusion === 'success' ? '✅' : '❌'} All imports have proper \`.js\` extensions | |
| - ${conclusion === 'success' ? '✅' : '❌'} Runtime import tests passed | |
| - ${conclusion === 'success' ? '✅' : '❌'} Functionality tests passed | |
| ### What This Validates | |
| The ESM validation suite ensures: | |
| 1. **Import Resolution**: All relative imports have proper \`.js\` extensions for Node.js ESM compatibility | |
| 2. **Directory Imports**: Directory imports correctly resolve to \`/index.js\` | |
| 3. **Package Structure**: ESM build includes \`package.json\` with \`"type": "module"\` | |
| 4. **Runtime Compatibility**: Package can be imported and used in Node.js 18, 20, and 22 | |
| 5. **Export Completeness**: All expected exports are accessible | |
| 6. **Functionality**: Imported code executes correctly | |
| ${conclusion === 'success' | |
| ? '✅ **The package is ready for ESM consumption!**' | |
| : '❌ **Please fix ESM issues before merging.**'} | |
| --- | |
| *This validation prevents issues like missing \`.js\` extensions, broken directory imports, and \`ERR_MODULE_NOT_FOUND\` errors.*`; | |
| // Find existing ESM validation comment | |
| const prNumber = parseInt(process.env.PR_NUMBER); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: prNumber, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('ESM Build Validation') | |
| ); | |
| if (botComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| comment_id: botComment.id, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| issue_number: prNumber, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| } | |
| # Final job to ensure all checks passed before allowing merge | |
| require-successful-checks: | |
| name: All Checks Passed | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| needs: | |
| - test | |
| - coverage-check | |
| - esm-validation-summary | |
| steps: | |
| - name: Verify all checks passed | |
| run: | | |
| echo "✅ All required checks have passed successfully!" | |
| echo "This PR is ready for review and merge." |