@@ -312,7 +312,7 @@ jobs:
312312 mysql -h 127.0.0.1 -P 3306 -u root -ptest_password -e "GRANT SELECT ON mysql.* TO 'test_user'@'%';"
313313 mysql -h 127.0.0.1 -P 3306 -u root -ptest_password -e "FLUSH PRIVILEGES;"
314314 echo "MySQL permissions granted"
315-
315+
316316 # Grant permissions for MariaDB
317317 mysql -h 127.0.0.1 -P 3307 -u root -ptest_password -e "GRANT SELECT, UPDATE ON performance_schema.* TO 'test_user'@'%';"
318318 mysql -h 127.0.0.1 -P 3307 -u root -ptest_password -e "GRANT SELECT ON mysql.* TO 'test_user'@'%';"
@@ -345,6 +345,174 @@ jobs:
345345 echo "⚠️ Coverage report not found"
346346 fi
347347
348+ coverage-gate :
349+ name : Coverage Gate
350+ runs-on : ubuntu-latest
351+ permissions :
352+ contents : read
353+ pull-requests : write
354+
355+ steps :
356+ - name : Checkout code
357+ uses : actions/checkout@v4
358+
359+ - name : Setup Node.js
360+ uses : actions/setup-node@v4
361+ with :
362+ node-version : ' 20.x'
363+ cache : ' npm'
364+
365+ - name : Install dependencies
366+ run : npm ci
367+
368+ - name : Run Jest tests with coverage
369+ run : npx jest --coverage --coverageReporters=json-summary --coverageReporters=text
370+ continue-on-error : true
371+
372+ - name : Check coverage threshold
373+ id : coverage_check
374+ run : |
375+ # Check if coverage summary exists
376+ if [ ! -f coverage/coverage-summary.json ]; then
377+ echo "❌ Coverage report not found"
378+ echo "status=error" >> $GITHUB_OUTPUT
379+ exit 1
380+ fi
381+
382+ # Extract coverage percentages
383+ LINES_PCT=$(node -e "const coverage = require('./coverage/coverage-summary.json'); console.log(coverage.total.lines.pct);")
384+ STATEMENTS_PCT=$(node -e "const coverage = require('./coverage/coverage-summary.json'); console.log(coverage.total.statements.pct);")
385+ BRANCHES_PCT=$(node -e "const coverage = require('./coverage/coverage-summary.json'); console.log(coverage.total.branches.pct);")
386+ FUNCTIONS_PCT=$(node -e "const coverage = require('./coverage/coverage-summary.json'); console.log(coverage.total.functions.pct);")
387+
388+ # Set minimum thresholds (matching jest.config.js)
389+ MIN_BRANCHES=33
390+ MIN_LINES=38
391+ MIN_STATEMENTS=39
392+ MIN_FUNCTIONS=39
393+
394+ echo "📊 Coverage Report:"
395+ echo " Lines: ${LINES_PCT}% (threshold: ${MIN_LINES}%)"
396+ echo " Statements: ${STATEMENTS_PCT}% (threshold: ${MIN_STATEMENTS}%)"
397+ echo " Branches: ${BRANCHES_PCT}% (threshold: ${MIN_BRANCHES}%)"
398+ echo " Functions: ${FUNCTIONS_PCT}% (threshold: ${MIN_FUNCTIONS}%)"
399+ echo ""
400+
401+ # Save to output for PR comment
402+ echo "lines_pct=$LINES_PCT" >> $GITHUB_OUTPUT
403+ echo "statements_pct=$STATEMENTS_PCT" >> $GITHUB_OUTPUT
404+ echo "branches_pct=$BRANCHES_PCT" >> $GITHUB_OUTPUT
405+ echo "functions_pct=$FUNCTIONS_PCT" >> $GITHUB_OUTPUT
406+ echo "min_lines=$MIN_LINES" >> $GITHUB_OUTPUT
407+ echo "min_statements=$MIN_STATEMENTS" >> $GITHUB_OUTPUT
408+ echo "min_branches=$MIN_BRANCHES" >> $GITHUB_OUTPUT
409+ echo "min_functions=$MIN_FUNCTIONS" >> $GITHUB_OUTPUT
410+
411+ # Check if any metric is below threshold
412+ FAILED=0
413+ if awk "BEGIN {exit !($LINES_PCT < $MIN_LINES)}"; then
414+ echo "❌ Lines coverage ($LINES_PCT%) is below minimum threshold ($MIN_LINES%)"
415+ FAILED=1
416+ fi
417+ if awk "BEGIN {exit !($STATEMENTS_PCT < $MIN_STATEMENTS)}"; then
418+ echo "❌ Statements coverage ($STATEMENTS_PCT%) is below minimum threshold ($MIN_STATEMENTS%)"
419+ FAILED=1
420+ fi
421+ if awk "BEGIN {exit !($BRANCHES_PCT < $MIN_BRANCHES)}"; then
422+ echo "❌ Branches coverage ($BRANCHES_PCT%) is below minimum threshold ($MIN_BRANCHES%)"
423+ FAILED=1
424+ fi
425+ if awk "BEGIN {exit !($FUNCTIONS_PCT < $MIN_FUNCTIONS)}"; then
426+ echo "❌ Functions coverage ($FUNCTIONS_PCT%) is below minimum threshold ($MIN_FUNCTIONS%)"
427+ FAILED=1
428+ fi
429+
430+ if [ $FAILED -eq 1 ]; then
431+ echo "status=failed" >> $GITHUB_OUTPUT
432+ echo ""
433+ echo "⚠️ Coverage gate FAILED. Please add tests to improve coverage."
434+ exit 1
435+ else
436+ echo "status=passed" >> $GITHUB_OUTPUT
437+ echo ""
438+ echo "✅ Coverage gate PASSED"
439+ fi
440+
441+ - name : Comment PR with coverage
442+ if : github.event_name == 'pull_request' && always()
443+ uses : actions/github-script@v7
444+ with :
445+ script : |
446+ const fs = require('fs');
447+
448+ let coverageComment = '## 📊 Coverage Report\n\n';
449+
450+ if (fs.existsSync('coverage/coverage-summary.json')) {
451+ const coverage = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8'));
452+ const total = coverage.total;
453+
454+ const status = '${{ steps.coverage_check.outputs.status }}';
455+ const statusEmoji = status === 'passed' ? '✅' : '❌';
456+
457+ coverageComment += `**Status:** ${statusEmoji} ${status === 'passed' ? 'PASSED' : 'FAILED'}\n\n`;
458+ coverageComment += '| Metric | Coverage | Threshold | Status |\n';
459+ coverageComment += '|--------|----------|-----------|--------|\n';
460+
461+ const metrics = [
462+ { name: 'Lines', pct: total.lines.pct, threshold: parseFloat('${{ steps.coverage_check.outputs.min_lines }}') },
463+ { name: 'Statements', pct: total.statements.pct, threshold: parseFloat('${{ steps.coverage_check.outputs.min_statements }}') },
464+ { name: 'Branches', pct: total.branches.pct, threshold: parseFloat('${{ steps.coverage_check.outputs.min_branches }}') },
465+ { name: 'Functions', pct: total.functions.pct, threshold: parseFloat('${{ steps.coverage_check.outputs.min_functions }}') }
466+ ];
467+
468+ metrics.forEach(metric => {
469+ const emoji = metric.pct >= metric.threshold ? '✅' : '❌';
470+ coverageComment += `| ${metric.name} | ${metric.pct.toFixed(2)}% | ${metric.threshold}% | ${emoji} |\n`;
471+ });
472+
473+ if (status !== 'passed') {
474+ coverageComment += `\n⚠️ **One or more coverage metrics are below their thresholds.** Please add tests to improve coverage.\n`;
475+ }
476+ } else {
477+ coverageComment += '❌ Coverage report not found.\n';
478+ }
479+
480+ // Find existing comment and update or create new
481+ const { data: comments } = await github.rest.issues.listComments({
482+ owner: context.repo.owner,
483+ repo: context.repo.repo,
484+ issue_number: context.issue.number,
485+ });
486+
487+ const botComment = comments.find(comment =>
488+ comment.user.type === 'Bot' &&
489+ comment.body.includes('📊 Coverage Report')
490+ );
491+
492+ if (botComment) {
493+ await github.rest.issues.updateComment({
494+ owner: context.repo.owner,
495+ repo: context.repo.repo,
496+ comment_id: botComment.id,
497+ body: coverageComment
498+ });
499+ } else {
500+ await github.rest.issues.createComment({
501+ owner: context.repo.owner,
502+ repo: context.repo.repo,
503+ issue_number: context.issue.number,
504+ body: coverageComment
505+ });
506+ }
507+
508+ - name : Upload coverage report
509+ if : always()
510+ uses : actions/upload-artifact@v4
511+ with :
512+ name : jest-coverage-report
513+ path : coverage/
514+ retention-days : 30
515+
348516 license-compliance :
349517 name : License Compliance Check
350518 runs-on : ubuntu-latest
@@ -378,7 +546,7 @@ jobs:
378546 status-check :
379547 name : Status Check
380548 runs-on : ubuntu-latest
381- needs : [build-and-test, package, lint-report, integration-tests-docker, license-compliance]
549+ needs : [build-and-test, package, lint-report, coverage-gate, integration-tests-docker, license-compliance]
382550 permissions :
383551 contents : read
384552 if : always()
@@ -395,6 +563,11 @@ jobs:
395563 echo "❌ Package failed"
396564 exit 1
397565 fi
566+ # Coverage gate
567+ if [ "${{ needs.coverage-gate.result }}" != "success" ]; then
568+ echo "❌ Coverage gate failed - minimum 39% coverage required"
569+ exit 1
570+ fi
398571 # Integration tests with Docker
399572 if [ "${{ needs.integration-tests-docker.result }}" != "success" ]; then
400573 echo "❌ Integration tests failed"
0 commit comments