Skip to content

ci: improve coverage summary (NYC table) #4

ci: improve coverage summary (NYC table)

ci: improve coverage summary (NYC table) #4

Workflow file for this run

name: PR - Test & Validate (Local)
on:
push:
branches: [main, staging, dev, develop]
pull_request_target:
branches: [main, staging, dev, develop]
types: [opened, reopened, synchronize]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20.x'
jobs:
dependency-review:
name: πŸ“‹ Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target' || github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
vulnerability-check: true
license-check: false
setup:
name: πŸ“¦ Setup Dependencies
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
cache-key: ${{ steps.setup-node.outputs.cache-key }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: true
ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Check package-lock.json consistency
continue-on-error: true
run: |
if [ -f package-lock.json ]; then
npm install --package-lock-only --no-audit --no-fund
git diff --exit-code package-lock.json || (
echo "::error::package-lock.json is out of sync with package.json. Run 'npm install' and commit the updated lockfile."
exit 1
)
fi
- name: Setup Node.js with cache
id: setup-node
uses: ./.github/actions/node-setup-cache
with:
node-version: ${{ env.NODE_VERSION }}
supply-chain:
name: πŸ” Supply Chain Security
runs-on: ubuntu-latest
timeout-minutes: 6
needs: setup
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: true
ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
- name: Restore Node.js and Cache
uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup.outputs.cache-key }}
- name: Verify package integrity (npm audit signatures)
run: |
echo "πŸ” Verifying npm package signatures..."
npm audit signatures 2>&1 || echo "::warning::Some packages may not have verified signatures (npm 10.7+ for full support)"
continue-on-error: true
- name: Check for known vulnerabilities (npm audit)
run: |
echo "πŸ›‘οΈ Checking for known vulnerabilities..."
npm audit --audit-level=high 2>&1 || echo "::warning::High severity vulnerabilities detected. Consider 'npm audit fix' or updating dependencies."
continue-on-error: true
lint:
name: ✨ Lint Code
runs-on: ubuntu-latest
timeout-minutes: 10
needs: setup
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: true
ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
- name: Restore Node.js and Cache
uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup.outputs.cache-key }}
- name: Run linting
continue-on-error: true
run: |
echo "Running linting..."
npm run lint --if-present || echo "::warning::Linting failed but continuing workflow"
- name: Run typecheck (optional)
continue-on-error: true
run: |
npm run typecheck --if-present || true
test-and-sonarqube:
name: πŸ§ͺ Test Coverage & SonarQube Analysis
runs-on: ubuntu-latest
timeout-minutes: 20
needs: setup
permissions:
contents: read
outputs:
coverage_result: ${{ steps.coverage-summary.outputs.coverage_result }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: true
fetch-depth: 0
ref: ${{ github.event.pull_request.number && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
- name: Restore Node.js and Cache
uses: ./.github/actions/node-restore-cache
with:
cache-key: ${{ needs.setup.outputs.cache-key }}
- name: Setup project configuration
run: |
echo "Setting up project configuration..."
if [ -f ./setup-config.sh ]; then
chmod +x ./setup-config.sh
./setup-config.sh
fi
- name: Run tests with coverage
id: run-tests
run: |
echo "Running tests with coverage..."
npm run test:coverage --if-present || npm test --if-present || echo "::warning::No test:coverage or test script; skipping"
- name: Coverage (threshold + summary)
id: coverage-summary
if: always()
run: |
MIN=80
# Vitest: detect by config (handles its own thresholds); brittle uses Istanbul + nyc check.
if [ -f vitest.config.js ] || [ -f vitest.config.ts ]; then
if [ -f coverage/coverage-final.json ]; then
if [ "${{ steps.run-tests.outcome }}" = "success" ]; then
STATUS_CELL="βœ… PASS"
else
STATUS_CELL="❌ FAIL (coverage below threshold in vitest.config.js)"
fi
echo "coverage_result=$STATUS_CELL Vitest (thresholds in vitest.config.js)" >> "$GITHUB_OUTPUT"
else
echo "coverage_result=⚠️ No coverage report" >> "$GITHUB_OUTPUT"
fi
elif [ -f coverage/coverage-final.json ]; then
mkdir -p .nyc_output
cp coverage/coverage-final.json .nyc_output/out.json
set +e
echo "Running nyc check-coverage (threshold ${MIN}%)..."
npx --yes nyc check-coverage --lines=$MIN --statements=$MIN --functions=$MIN --branches=$MIN
EXIT=$?
set -e
echo "Running nyc report (text-summary)..."
NYC_REPORT=$(npx --yes nyc report --reporter=text-summary 2>&1)
echo "$NYC_REPORT"
STMT=$(echo "$NYC_REPORT" | sed -n 's/^Statements[^:]*: *\([0-9.]*\)%.*/\1/p')
BRANCH=$(echo "$NYC_REPORT" | sed -n 's/^Branches[^:]*: *\([0-9.]*\)%.*/\1/p')
FN=$(echo "$NYC_REPORT" | sed -n 's/^Functions[^:]*: *\([0-9.]*\)%.*/\1/p')
LINES=$(echo "$NYC_REPORT" | sed -n 's/^Lines[^:]*: *\([0-9.]*\)%.*/\1/p')
[ -z "$STMT" ] && STMT="β€”"; [ -z "$BRANCH" ] && BRANCH="β€”"; [ -z "$FN" ] && FN="β€”"; [ -z "$LINES" ] && LINES="β€”"
[ $EXIT -eq 0 ] && ICON="βœ…" || ICON="❌"
echo "coverage_result=$ICON Stmt ${STMT}% | Branch ${BRANCH}% | Fn ${FN}% | Lines ${LINES}% (min ${MIN}%)" >> "$GITHUB_OUTPUT"
exit $EXIT
else
echo "coverage_result=⚠️ No coverage report" >> "$GITHUB_OUTPUT"
fi
- name: Run build (optional)
continue-on-error: true
run: npm run build --if-present || true
- name: Tailscale
id: tailscale
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
tags: tag:ci-sast
version: latest
ping: dev-sonarcube-0.tail8a2a3f.ts.net
- name: Generate ESLint JSON report
if: steps.tailscale.outcome == 'success'
continue-on-error: true
run: |
if [ -f eslint.config.cjs ]; then
npx eslint . --format json --output-file eslint-report.json
fi
- name: Prepare SonarQube Configuration
if: steps.tailscale.outcome == 'success'
run: |
echo "Creating sonar-project.properties..."
# Base configuration
cat > sonar-project.properties << 'SONAR_EOF'
sonar.projectKey=${{ github.event.repository.name }}
sonar.projectName=${{ github.event.repository.name }}
sonar.projectVersion=${{ github.sha }}
sonar.sources=.
sonar.language=js
sonar.sourceEncoding=UTF-8
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.javascript.node.maxspace=4096
sonar.javascript.environments=node
sonar.qualitygate.wait=true
sonar.test.inclusions=**/*.test.js,**/*.spec.js
SONAR_EOF
# Add ESLint report if it exists
if [ -f eslint-report.json ]; then
echo "sonar.eslint.reportPaths=eslint-report.json" >> sonar-project.properties
fi
# Base exclusions
BASE_EXCLUSIONS="**/node_modules/**,**/coverage/**,**/dist/**,**/build/**,.github/**,**/*.test.js,**/*.spec.js,**/*.json,**/*.md,**/*.yml,**/*.yaml"
# Add tests configuration if folder exists
if [ -d "tests" ]; then
echo "Tests folder found - including test configuration"
echo "sonar.tests=tests" >> sonar-project.properties
echo "sonar.exclusions=${BASE_EXCLUSIONS},**/tests/**" >> sonar-project.properties
echo "sonar.cpd.exclusions=**/tests/**,**/node_modules/**" >> sonar-project.properties
else
echo "No tests folder found - skipping test configuration"
echo "sonar.exclusions=${BASE_EXCLUSIONS}" >> sonar-project.properties
fi
echo "Configuration file created"
- name: SonarQube Scan
if: steps.tailscale.outcome == 'success'
continue-on-error: true
uses: SonarSource/sonarqube-scan-action@v7.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
summary:
name: πŸ“‹ CI Summary
runs-on: ubuntu-latest
needs: [setup, supply-chain, lint, test-and-sonarqube]
if: always() && !cancelled()
permissions:
contents: read
steps:
- name: πŸ“₯ Checkout for summary
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: πŸ“Š Print summary
env:
R_SETUP: ${{ needs.setup.result || 'skipped' }}
R_SUPPLY: ${{ needs.supply-chain.result || 'skipped' }}
R_LINT: ${{ needs.lint.result || 'skipped' }}
R_TEST: ${{ needs.test-and-sonarqube.result || 'skipped' }}
COVERAGE: ${{ needs.test-and-sonarqube.outputs.coverage_result || 'β€”' }}
run: |
{
echo "## πŸ“Š CI Pipeline Summary"
echo ""
echo "### πŸ“‹ Commit information"
echo "- **Commit:** $(git rev-parse HEAD)"
echo "- **Message:** $(git log -1 --pretty=format:'%s')"
echo "- **Author:** $(git log -1 --pretty=format:'%an <%ae>')"
echo "- **Date:** $(git log -1 --pretty=format:'%ad' --date=short)"
echo ""
echo "### 🎯 Job results"
[ "${R_SETUP}" != "skipped" ] && echo "- πŸ“¦ Setup: ${R_SETUP:-?}"
[ "${R_SUPPLY}" != "skipped" ] && echo "- πŸ” Supply Chain: ${R_SUPPLY:-?}"
[ "${R_LINT}" != "skipped" ] && echo "- ✨ Lint: ${R_LINT:-?}"
[ "${R_TEST}" != "skipped" ] && echo "- πŸ§ͺ Test & SonarQube: ${R_TEST:-?}"
echo "- πŸ“Š Coverage: ${COVERAGE}"
echo ""
echo "### πŸ”§ Pipeline"
echo "- βœ… Dependency review (PR only)"
echo "- βœ… Lockfile check, npm audit, lint, tests"
echo "- βœ… SonarQube (push to main or manual run)"
} | tee -a "$GITHUB_STEP_SUMMARY"