Skip to content

chore(deps)(deps-dev): bump @vitest/coverage-v8 from 3.2.4 to 4.0.18 in /packages/testing #308

chore(deps)(deps-dev): bump @vitest/coverage-v8 from 3.2.4 to 4.0.18 in /packages/testing

chore(deps)(deps-dev): bump @vitest/coverage-v8 from 3.2.4 to 4.0.18 in /packages/testing #308

Workflow file for this run

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
});
}