chore(deps)(deps-dev): bump @vitest/coverage-v8 from 3.2.4 to 4.0.18 in /packages/testing #308
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, development] | |
| pull_request: | |
| branches: [main, development] | |
| # Cancel in-progress runs for the same workflow and PR/branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| actions: read | |
| contents: read | |
| security-events: write | |
| pull-requests: write | |
| jobs: | |
| # ============================================================================ | |
| # STAGE 1: Unit Tests & Coverage | |
| # ============================================================================ | |
| # This job runs first and gates all subsequent jobs | |
| # If tests fail, the entire pipeline stops here to save resources | |
| test: | |
| name: Unit Tests & Coverage | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 # Fetch all history for better coverage comparison | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run linting | |
| run: npx lerna run code:check:ci --stream | |
| - name: Build all packages | |
| run: npm run build:ci | |
| - name: Run unit tests with coverage | |
| run: npx lerna run test:coverage --stream | |
| - name: Upload coverage reports to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./packages/ioc/coverage/lcov.info,./packages/testing/coverage/lcov.info,./packages/cli/coverage/lcov.info | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| verbose: true | |
| - name: Archive test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results | |
| path: | | |
| packages/*/coverage | |
| packages/*/test-results | |
| retention-days: 7 | |
| - name: Upload build artifacts for coverage-check | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-artifacts | |
| path: | | |
| node_modules | |
| packages/*/dist | |
| packages/*/node_modules | |
| retention-days: 1 | |
| # ============================================================================ | |
| # STAGE 2: Coverage Protection Check | |
| # ============================================================================ | |
| # This job runs only if unit tests pass | |
| # Compares coverage between PR and main branch | |
| coverage-check: | |
| name: Coverage Protection Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: [test] # ⚠️ Depends on test job - won't run if tests fail | |
| # Run only on pull_request events | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Checkout PR code | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download build artifacts from test job | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: build-artifacts | |
| - name: Download test results from test job | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: test-results | |
| - name: Save PR coverage | |
| run: | | |
| echo "Saving PR coverage summaries..." | |
| mkdir -p coverage-reports/pr | |
| if [ -f packages/ioc/coverage/coverage-summary.json ]; then | |
| cp packages/ioc/coverage/coverage-summary.json coverage-reports/pr/ioc-coverage.json | |
| fi | |
| if [ -f packages/testing/coverage/coverage-summary.json ]; then | |
| cp packages/testing/coverage/coverage-summary.json coverage-reports/pr/testing-coverage.json | |
| fi | |
| if [ -f packages/cli/coverage/coverage-summary.json ]; then | |
| cp packages/cli/coverage/coverage-summary.json coverage-reports/pr/cli-coverage.json | |
| fi | |
| - name: Checkout main branch | |
| run: | | |
| git fetch origin main | |
| git checkout main | |
| - name: Setup Node.js for main branch | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| cache: 'npm' | |
| cache-dependency-path: package-lock.json | |
| - name: Install dependencies on main | |
| run: npm ci | |
| - name: Run tests with coverage on main branch | |
| run: npx lerna run test:coverage --stream | |
| continue-on-error: true | |
| - name: Save main coverage | |
| run: | | |
| echo "Saving main coverage summaries..." | |
| mkdir -p coverage-reports/main | |
| if [ -f packages/ioc/coverage/coverage-summary.json ]; then | |
| cp packages/ioc/coverage/coverage-summary.json coverage-reports/main/ioc-coverage.json | |
| echo "MAIN_BRANCH_HAS_IOC_COVERAGE=true" >> "$GITHUB_ENV" | |
| else | |
| echo "⚠️ IoC coverage summary not found for main branch" | |
| 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-reports/main/ioc-coverage.json | |
| echo "MAIN_BRANCH_HAS_IOC_COVERAGE=false" >> "$GITHUB_ENV" | |
| fi | |
| if [ -f packages/testing/coverage/coverage-summary.json ]; then | |
| cp packages/testing/coverage/coverage-summary.json coverage-reports/main/testing-coverage.json | |
| echo "MAIN_BRANCH_HAS_TESTING_COVERAGE=true" >> "$GITHUB_ENV" | |
| else | |
| echo "⚠️ Testing coverage summary not found for main branch" | |
| 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-reports/main/testing-coverage.json | |
| echo "MAIN_BRANCH_HAS_TESTING_COVERAGE=false" >> "$GITHUB_ENV" | |
| fi | |
| if [ -f packages/cli/coverage/coverage-summary.json ]; then | |
| cp packages/cli/coverage/coverage-summary.json coverage-reports/main/cli-coverage.json | |
| echo "MAIN_BRANCH_HAS_CLI_COVERAGE=true" >> "$GITHUB_ENV" | |
| else | |
| echo "⚠️ CLI coverage summary not found for main branch" | |
| 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-reports/main/cli-coverage.json | |
| echo "MAIN_BRANCH_HAS_CLI_COVERAGE=false" >> "$GITHUB_ENV" | |
| fi | |
| - name: Compare coverage and create report | |
| run: | | |
| echo "📊 Comparing coverage between main and PR..." | |
| # Minimum coverage thresholds (absolute minimums) | |
| MIN_LINES_THRESHOLD=70 | |
| MIN_STATEMENTS_THRESHOLD=70 | |
| MIN_FUNCTIONS_THRESHOLD=60 | |
| MIN_BRANCHES_THRESHOLD=60 | |
| # Function to compare coverage for a package | |
| compare_package_coverage() { | |
| local package_name=$1 | |
| local main_file="coverage-reports/main/${package_name}-coverage.json" | |
| local pr_file="coverage-reports/pr/${package_name}-coverage.json" | |
| if [ ! -f "$pr_file" ]; then | |
| echo "⚠️ No coverage found for ${package_name} in PR" | |
| return 0 | |
| fi | |
| # Extract coverage percentages using jq | |
| MAIN_LINES=$(jq -r '.total.lines.pct // 0' "$main_file") | |
| MAIN_STATEMENTS=$(jq -r '.total.statements.pct // 0' "$main_file") | |
| MAIN_FUNCTIONS=$(jq -r '.total.functions.pct // 0' "$main_file") | |
| MAIN_BRANCHES=$(jq -r '.total.branches.pct // 0' "$main_file") | |
| PR_LINES=$(jq -r '.total.lines.pct // 0' "$pr_file") | |
| PR_STATEMENTS=$(jq -r '.total.statements.pct // 0' "$pr_file") | |
| PR_FUNCTIONS=$(jq -r '.total.functions.pct // 0' "$pr_file") | |
| PR_BRANCHES=$(jq -r '.total.branches.pct // 0' "$pr_file") | |
| echo "" | |
| echo "=== ${package_name} Package ===" | |
| 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 | |
| local failed=0 | |
| # Check against main branch | |
| 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 ${PR_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 | |
| # Check against absolute minimum thresholds | |
| echo "" | |
| echo "Checking against minimum thresholds..." | |
| if [ $(echo "$PR_LINES < $MIN_LINES_THRESHOLD" | bc -l) -eq 1 ]; then | |
| echo "❌ Lines coverage ${PR_LINES}% is below minimum threshold ${MIN_LINES_THRESHOLD}%" | |
| failed=1 | |
| else | |
| echo "✅ Lines coverage ${PR_LINES}% meets minimum threshold ${MIN_LINES_THRESHOLD}%" | |
| fi | |
| if [ $(echo "$PR_STATEMENTS < $MIN_STATEMENTS_THRESHOLD" | bc -l) -eq 1 ]; then | |
| echo "❌ Statements coverage ${PR_STATEMENTS}% is below minimum threshold ${MIN_STATEMENTS_THRESHOLD}%" | |
| failed=1 | |
| else | |
| echo "✅ Statements coverage ${PR_STATEMENTS}% meets minimum threshold ${MIN_STATEMENTS_THRESHOLD}%" | |
| fi | |
| if [ $(echo "$PR_FUNCTIONS < $MIN_FUNCTIONS_THRESHOLD" | bc -l) -eq 1 ]; then | |
| echo "❌ Functions coverage ${PR_FUNCTIONS}% is below minimum threshold ${MIN_FUNCTIONS_THRESHOLD}%" | |
| failed=1 | |
| else | |
| echo "✅ Functions coverage ${PR_FUNCTIONS}% meets minimum threshold ${MIN_FUNCTIONS_THRESHOLD}%" | |
| fi | |
| if [ $(echo "$PR_BRANCHES < $MIN_BRANCHES_THRESHOLD" | bc -l) -eq 1 ]; then | |
| echo "❌ Branches coverage ${PR_BRANCHES}% is below minimum threshold ${MIN_BRANCHES_THRESHOLD}%" | |
| failed=1 | |
| else | |
| echo "✅ Branches coverage ${PR_BRANCHES}% meets minimum threshold ${MIN_BRANCHES_THRESHOLD}%" | |
| fi | |
| # Save coverage values for this package | |
| echo "${package_name^^}_MAIN_LINES=${MAIN_LINES}" >> "$GITHUB_ENV" | |
| echo "${package_name^^}_MAIN_STATEMENTS=${MAIN_STATEMENTS}" >> "$GITHUB_ENV" | |
| echo "${package_name^^}_MAIN_FUNCTIONS=${MAIN_FUNCTIONS}" >> "$GITHUB_ENV" | |
| echo "${package_name^^}_MAIN_BRANCHES=${MAIN_BRANCHES}" >> "$GITHUB_ENV" | |
| echo "${package_name^^}_PR_LINES=${PR_LINES}" >> "$GITHUB_ENV" | |
| echo "${package_name^^}_PR_STATEMENTS=${PR_STATEMENTS}" >> "$GITHUB_ENV" | |
| echo "${package_name^^}_PR_FUNCTIONS=${PR_FUNCTIONS}" >> "$GITHUB_ENV" | |
| echo "${package_name^^}_PR_BRANCHES=${PR_BRANCHES}" >> "$GITHUB_ENV" | |
| return $failed | |
| } | |
| # Compare coverage for each package | |
| OVERALL_FAILED=0 | |
| compare_package_coverage "ioc" || OVERALL_FAILED=1 | |
| compare_package_coverage "testing" || OVERALL_FAILED=1 | |
| compare_package_coverage "cli" || OVERALL_FAILED=1 | |
| echo "" | |
| if [ $OVERALL_FAILED -eq 1 ]; then | |
| echo "❌ Coverage check FAILED: Coverage has decreased for one or more packages" | |
| echo "COVERAGE_STATUS=failed" >> "$GITHUB_ENV" | |
| echo "COVERAGE_FAILED=1" >> "$GITHUB_ENV" | |
| else | |
| echo "✅ Coverage check PASSED: Coverage maintained or improved for all packages" | |
| echo "COVERAGE_STATUS=passed" >> "$GITHUB_ENV" | |
| echo "COVERAGE_FAILED=0" >> "$GITHUB_ENV" | |
| fi | |
| - name: Comment PR with coverage comparison | |
| if: always() | |
| uses: actions/github-script@v8 | |
| env: | |
| COVERAGE_FAILED: ${{ env.COVERAGE_FAILED }} | |
| MAIN_BRANCH_HAS_IOC_COVERAGE: ${{ env.MAIN_BRANCH_HAS_IOC_COVERAGE }} | |
| MAIN_BRANCH_HAS_TESTING_COVERAGE: ${{ env.MAIN_BRANCH_HAS_TESTING_COVERAGE }} | |
| MAIN_BRANCH_HAS_CLI_COVERAGE: ${{ env.MAIN_BRANCH_HAS_CLI_COVERAGE }} | |
| IOC_MAIN_LINES: ${{ env.IOC_MAIN_LINES }} | |
| IOC_MAIN_STATEMENTS: ${{ env.IOC_MAIN_STATEMENTS }} | |
| IOC_MAIN_FUNCTIONS: ${{ env.IOC_MAIN_FUNCTIONS }} | |
| IOC_MAIN_BRANCHES: ${{ env.IOC_MAIN_BRANCHES }} | |
| IOC_PR_LINES: ${{ env.IOC_PR_LINES }} | |
| IOC_PR_STATEMENTS: ${{ env.IOC_PR_STATEMENTS }} | |
| IOC_PR_FUNCTIONS: ${{ env.IOC_PR_FUNCTIONS }} | |
| IOC_PR_BRANCHES: ${{ env.IOC_PR_BRANCHES }} | |
| TESTING_MAIN_LINES: ${{ env.TESTING_MAIN_LINES }} | |
| TESTING_MAIN_STATEMENTS: ${{ env.TESTING_MAIN_STATEMENTS }} | |
| TESTING_MAIN_FUNCTIONS: ${{ env.TESTING_MAIN_FUNCTIONS }} | |
| TESTING_MAIN_BRANCHES: ${{ env.TESTING_MAIN_BRANCHES }} | |
| TESTING_PR_LINES: ${{ env.TESTING_PR_LINES }} | |
| TESTING_PR_STATEMENTS: ${{ env.TESTING_PR_STATEMENTS }} | |
| TESTING_PR_FUNCTIONS: ${{ env.TESTING_PR_FUNCTIONS }} | |
| TESTING_PR_BRANCHES: ${{ env.TESTING_PR_BRANCHES }} | |
| CLI_MAIN_LINES: ${{ env.CLI_MAIN_LINES }} | |
| CLI_MAIN_STATEMENTS: ${{ env.CLI_MAIN_STATEMENTS }} | |
| CLI_MAIN_FUNCTIONS: ${{ env.CLI_MAIN_FUNCTIONS }} | |
| CLI_MAIN_BRANCHES: ${{ env.CLI_MAIN_BRANCHES }} | |
| CLI_PR_LINES: ${{ env.CLI_PR_LINES }} | |
| CLI_PR_STATEMENTS: ${{ env.CLI_PR_STATEMENTS }} | |
| CLI_PR_FUNCTIONS: ${{ env.CLI_PR_FUNCTIONS }} | |
| CLI_PR_BRANCHES: ${{ env.CLI_PR_BRANCHES }} | |
| with: | |
| script: | | |
| const failed = process.env.COVERAGE_FAILED === '1'; | |
| const mainHasIocCoverage = process.env.MAIN_BRANCH_HAS_IOC_COVERAGE === 'true'; | |
| const mainHasTestingCoverage = process.env.MAIN_BRANCH_HAS_TESTING_COVERAGE === 'true'; | |
| const mainHasCliCoverage = process.env.MAIN_BRANCH_HAS_CLI_COVERAGE === 'true'; | |
| const isFirstTime = !mainHasIocCoverage && !mainHasTestingCoverage && !mainHasCliCoverage; | |
| 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 formatDiff = (pr, main) => { | |
| const diff = parseFloat(pr) - parseFloat(main); | |
| const sign = diff > 0 ? '+' : ''; | |
| return `${sign}${diff.toFixed(2)}%`; | |
| }; | |
| const statusIcon = failed ? '❌' : '✅'; | |
| const statusText = failed ? 'FAILED - Coverage Decreased' : 'PASSED - Coverage Maintained'; | |
| let body; | |
| if (isFirstTime) { | |
| body = `## ℹ️ Code Coverage Protection - First Time Setup | |
| **Status:** ✅ PASSED (Initial Setup) | |
| ### @nexus-ioc/core Coverage | |
| | Metric | This PR | | |
| |--------|---------| | |
| | Lines | ${process.env.IOC_PR_LINES}% | | |
| | Statements | ${process.env.IOC_PR_STATEMENTS}% | | |
| | Functions | ${process.env.IOC_PR_FUNCTIONS}% | | |
| | Branches | ${process.env.IOC_PR_BRANCHES}% | | |
| ### @nexus-ioc/testing Coverage | |
| | Metric | This PR | | |
| |--------|---------| | |
| | Lines | ${process.env.TESTING_PR_LINES}% | | |
| | Statements | ${process.env.TESTING_PR_STATEMENTS}% | | |
| | Functions | ${process.env.TESTING_PR_FUNCTIONS}% | | |
| | Branches | ${process.env.TESTING_PR_BRANCHES}% | | |
| ### @nexus-ioc/cli Coverage | |
| | Metric | This PR | | |
| |--------|---------| | |
| | Lines | ${process.env.CLI_PR_LINES}% | | |
| | Statements | ${process.env.CLI_PR_STATEMENTS}% | | |
| | Functions | ${process.env.CLI_PR_FUNCTIONS}% | | |
| | Branches | ${process.env.CLI_PR_BRANCHES}% | | |
| ### 📝 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 { | |
| body = `## ${statusIcon} Code Coverage Protection | |
| **Status:** ${statusText} | |
| ### @nexus-ioc/core Coverage | |
| | Metric | Main Branch | This PR | Change | Status | | |
| |--------|-------------|---------|--------|--------| | |
| | Lines | ${process.env.IOC_MAIN_LINES}% | ${process.env.IOC_PR_LINES}% | ${getIcon(process.env.IOC_PR_LINES, process.env.IOC_MAIN_LINES)} ${formatDiff(process.env.IOC_PR_LINES, process.env.IOC_MAIN_LINES)} | ${getStatus(process.env.IOC_PR_LINES, process.env.IOC_MAIN_LINES)} | | |
| | Statements | ${process.env.IOC_MAIN_STATEMENTS}% | ${process.env.IOC_PR_STATEMENTS}% | ${getIcon(process.env.IOC_PR_STATEMENTS, process.env.IOC_MAIN_STATEMENTS)} ${formatDiff(process.env.IOC_PR_STATEMENTS, process.env.IOC_MAIN_STATEMENTS)} | ${getStatus(process.env.IOC_PR_STATEMENTS, process.env.IOC_MAIN_STATEMENTS)} | | |
| | Functions | ${process.env.IOC_MAIN_FUNCTIONS}% | ${process.env.IOC_PR_FUNCTIONS}% | ${getIcon(process.env.IOC_PR_FUNCTIONS, process.env.IOC_MAIN_FUNCTIONS)} ${formatDiff(process.env.IOC_PR_FUNCTIONS, process.env.IOC_MAIN_FUNCTIONS)} | ${getStatus(process.env.IOC_PR_FUNCTIONS, process.env.IOC_MAIN_FUNCTIONS)} | | |
| | Branches | ${process.env.IOC_MAIN_BRANCHES}% | ${process.env.IOC_PR_BRANCHES}% | ${getIcon(process.env.IOC_PR_BRANCHES, process.env.IOC_MAIN_BRANCHES)} ${formatDiff(process.env.IOC_PR_BRANCHES, process.env.IOC_MAIN_BRANCHES)} | ${getStatus(process.env.IOC_PR_BRANCHES, process.env.IOC_MAIN_BRANCHES)} | | |
| ### @nexus-ioc/testing Coverage | |
| | Metric | Main Branch | This PR | Change | Status | | |
| |--------|-------------|---------|--------|--------| | |
| | Lines | ${process.env.TESTING_MAIN_LINES}% | ${process.env.TESTING_PR_LINES}% | ${getIcon(process.env.TESTING_PR_LINES, process.env.TESTING_MAIN_LINES)} ${formatDiff(process.env.TESTING_PR_LINES, process.env.TESTING_MAIN_LINES)} | ${getStatus(process.env.TESTING_PR_LINES, process.env.TESTING_MAIN_LINES)} | | |
| | Statements | ${process.env.TESTING_MAIN_STATEMENTS}% | ${process.env.TESTING_PR_STATEMENTS}% | ${getIcon(process.env.TESTING_PR_STATEMENTS, process.env.TESTING_MAIN_STATEMENTS)} ${formatDiff(process.env.TESTING_PR_STATEMENTS, process.env.TESTING_MAIN_STATEMENTS)} | ${getStatus(process.env.TESTING_PR_STATEMENTS, process.env.TESTING_MAIN_STATEMENTS)} | | |
| | Functions | ${process.env.TESTING_MAIN_FUNCTIONS}% | ${process.env.TESTING_PR_FUNCTIONS}% | ${getIcon(process.env.TESTING_PR_FUNCTIONS, process.env.TESTING_MAIN_FUNCTIONS)} ${formatDiff(process.env.TESTING_PR_FUNCTIONS, process.env.TESTING_MAIN_FUNCTIONS)} | ${getStatus(process.env.TESTING_PR_FUNCTIONS, process.env.TESTING_MAIN_FUNCTIONS)} | | |
| | Branches | ${process.env.TESTING_MAIN_BRANCHES}% | ${process.env.TESTING_PR_BRANCHES}% | ${getIcon(process.env.TESTING_PR_BRANCHES, process.env.TESTING_MAIN_BRANCHES)} ${formatDiff(process.env.TESTING_PR_BRANCHES, process.env.TESTING_MAIN_BRANCHES)} | ${getStatus(process.env.TESTING_PR_BRANCHES, process.env.TESTING_MAIN_BRANCHES)} | | |
| ### @nexus-ioc/cli Coverage | |
| | Metric | Main Branch | This PR | Change | Status | | |
| |--------|-------------|---------|--------|--------| | |
| | Lines | ${process.env.CLI_MAIN_LINES}% | ${process.env.CLI_PR_LINES}% | ${getIcon(process.env.CLI_PR_LINES, process.env.CLI_MAIN_LINES)} ${formatDiff(process.env.CLI_PR_LINES, process.env.CLI_MAIN_LINES)} | ${getStatus(process.env.CLI_PR_LINES, process.env.CLI_MAIN_LINES)} | | |
| | Statements | ${process.env.CLI_MAIN_STATEMENTS}% | ${process.env.CLI_PR_STATEMENTS}% | ${getIcon(process.env.CLI_PR_STATEMENTS, process.env.CLI_MAIN_STATEMENTS)} ${formatDiff(process.env.CLI_PR_STATEMENTS, process.env.CLI_MAIN_STATEMENTS)} | ${getStatus(process.env.CLI_PR_STATEMENTS, process.env.CLI_MAIN_STATEMENTS)} | | |
| | Functions | ${process.env.CLI_MAIN_FUNCTIONS}% | ${process.env.CLI_PR_FUNCTIONS}% | ${getIcon(process.env.CLI_PR_FUNCTIONS, process.env.CLI_MAIN_FUNCTIONS)} ${formatDiff(process.env.CLI_PR_FUNCTIONS, process.env.CLI_MAIN_FUNCTIONS)} | ${getStatus(process.env.CLI_PR_FUNCTIONS, process.env.CLI_MAIN_FUNCTIONS)} | | |
| | Branches | ${process.env.CLI_MAIN_BRANCHES}% | ${process.env.CLI_PR_BRANCHES}% | ${getIcon(process.env.CLI_PR_BRANCHES, process.env.CLI_MAIN_BRANCHES)} ${formatDiff(process.env.CLI_PR_BRANCHES, process.env.CLI_MAIN_BRANCHES)} | ${getStatus(process.env.CLI_PR_BRANCHES, process.env.CLI_MAIN_BRANCHES)} | | |
| ${failed | |
| ? '### ⚠️ Action Required\n\nThis PR decreases code coverage for one or more packages. 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 for all packages. This PR is ready for review.'} | |
| --- | |
| *Coverage protection is enabled. PRs that decrease coverage will be blocked from merging.*`; | |
| } | |
| // Find existing coverage comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('Code Coverage Protection') | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| comment_id: botComment.id, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| 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 | |
| # ============================================================================ | |
| # STAGE 3: Build Validation (Matrix) | |
| # ============================================================================ | |
| # This job runs only if coverage check passes (or is skipped for non-PR events) | |
| # Tests build across multiple Node.js versions in parallel | |
| build-validation: | |
| name: Build Validation | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: [test, coverage-check] # ⚠️ Depends on test and coverage-check jobs | |
| # Run always for push events, but for PRs only if coverage-check passes or is skipped | |
| if: always() && (needs.test.result == 'success') && (github.event_name == 'push' || needs.coverage-check.result == 'success' || needs.coverage-check.result == 'skipped') | |
| strategy: | |
| fail-fast: true # Stop all matrix jobs if one fails | |
| matrix: | |
| node-version: [18, 20, 22] # Test on multiple Node.js versions | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Setup Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build all packages | |
| run: npm run build:all | |
| - name: Verify build artifacts exist | |
| run: | | |
| echo "Checking build artifacts..." | |
| test -d packages/ioc/dist || (echo "❌ IoC build directory not found" && exit 1) | |
| test -d packages/testing/dist || (echo "❌ Testing build directory not found" && exit 1) | |
| test -d packages/cli/dist || (echo "❌ CLI build directory not found" && exit 1) | |
| test -d packages/shared/dist || (echo "❌ Shared build directory not found" && exit 1) | |
| echo "✅ Build artifacts verified" | |
| - name: Test package imports (smoke test) | |
| run: | | |
| echo "Testing package imports..." | |
| node -e "require('reflect-metadata'); const ioc = require('@nexus-ioc/core'); console.log('✅ IoC package import successful');" | |
| node -e "require('reflect-metadata'); const testing = require('@nexus-ioc/testing'); console.log('✅ Testing package import successful');" | |
| # CLI package is designed to run as a command-line tool, not to be imported | |
| # Test that the CLI binary exists and is executable | |
| test -f packages/cli/dist/index.js && echo "✅ CLI package build successful" | |
| echo "✅ All package imports successful" | |
| - name: Upload build artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-artifacts-node-${{ matrix.node-version }} | |
| path: | | |
| packages/*/dist/**/*.js | |
| packages/*/dist/**/*.d.ts | |
| retention-days: 7 | |
| # ============================================================================ | |
| # STAGE 3: PR Validation | |
| # ============================================================================ | |
| # This job runs only if build validation passes | |
| # CodeQL security analysis runs separately via scheduled workflow (codeql.yml) | |
| pr-validation: | |
| name: PR Validation Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: [build-validation, coverage-check] # ⚠️ Depends on build-validation and coverage-check | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Comment PR with validation results | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const testResult = '${{ needs.test.result }}'; | |
| const coverageResult = '${{ needs.coverage-check.result }}'; | |
| const buildResult = '${{ needs.build-validation.result }}'; | |
| const allSuccess = testResult === 'success' && coverageResult === 'success' && buildResult === 'success'; | |
| const statusIcon = allSuccess ? '✅' : '❌'; | |
| const statusText = allSuccess ? 'All validation checks passed!' : 'Validation failed!'; | |
| const body = `## ${statusIcon} Pull Request Validation | |
| **Status:** ${statusText} | |
| ### Pipeline Stages | |
| | Stage | Status | | |
| |-------|--------| | |
| | Unit Tests & Coverage | ${testResult === 'success' ? '✅ Passed' : '❌ Failed'} | | |
| | Coverage Protection Check | ${coverageResult === 'success' ? '✅ Passed' : coverageResult === 'skipped' ? '⏭️ Skipped' : '❌ Failed'} | | |
| | Build Validation (Node 18, 20, 22) | ${buildResult === 'success' ? '✅ Passed' : '❌ Failed'} | | |
| ### Checks Performed | |
| - ${testResult === 'success' ? '✅' : '❌'} Code linting (Biome) | |
| - ${testResult === 'success' ? '✅' : '❌'} Unit tests with coverage | |
| - ${coverageResult === 'success' ? '✅' : coverageResult === 'skipped' ? '⏭️' : '❌'} Coverage protection (no coverage decrease) | |
| - ${buildResult === 'success' ? '✅' : '❌'} Build all packages (Node 18, 20, 22) | |
| - ${buildResult === 'success' ? '✅' : '❌'} Package imports verification | |
| - ${buildResult === 'success' ? '✅' : '❌'} TypeScript declarations | |
| ### Packages Validated | |
| - @nexus-ioc/core | |
| - @nexus-ioc/testing | |
| - @nexus-ioc/cli | |
| - @nexus-ioc/shared | |
| ${allSuccess | |
| ? '✅ **This PR is ready for review!**' | |
| : '❌ **Please fix the issues before requesting review.**'} | |
| --- | |
| *Automated validation by GitHub Actions - Sequential pipeline for resource optimization*`; | |
| // Find existing validation comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('Pull Request Validation') | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| comment_id: botComment.id, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: body | |
| }); | |
| } | |