name: Contracts - Test & Coverage
on: workflow_call: inputs: runner: description: 'Runner type' required: false type: string default: 'ubuntu-latest' node_version: description: 'Node.js version' required: false type: string default: '22' package_manager: description: 'Package manager (npm, yarn, or pnpm)' required: false type: string default: 'pnpm' pnpm_version: description: 'pnpm version (used when package_manager: pnpm)' required: false type: string default: '10' artifact_name: description: 'Name of build artifact to download (compiled contracts). Empty = recompile.' required: false type: string default: '' run_coverage: description: 'Run solidity-coverage' required: false type: boolean default: false coverage_threshold: description: 'Minimum line coverage percentage (0-100). 0 = disabled.' required: false type: number default: 0 run_gas_reporter: description: 'Enable hardhat-gas-reporter (REPORT_GAS=true)' required: false type: boolean default: false upload_reports: description: 'Upload coverage and gas reports as artifacts' required: false type: boolean default: false
outputs: result: description: 'Test result (success/failure)' value: ${{ jobs.test.outputs.result }} coverage_percentage: description: 'Line coverage percentage' value: ${{ jobs.test.outputs.coverage }} tests_passed: description: 'Number of tests passed' value: ${{ jobs.test.outputs.passed }} tests_failed: description: 'Number of tests failed' value: ${{ jobs.test.outputs.failed }}
jobs: test: name: Test & Coverage runs-on: ${{ inputs.runner }} timeout-minutes: 30
outputs: result: ${{ steps.test-run.outcome }} coverage: ${{ steps.coverage.outputs.percentage }} passed: ${{ steps.test-summary.outputs.passed }} failed: ${{ steps.test-summary.outputs.failed }}
steps: - name: Checkout uses: actions/checkout@v5
- name: Setup pnpm if: inputs.package_manager == 'pnpm' uses: pnpm/action-setup@v4
- name: Setup Node.js ${{ inputs.node_version }} uses: actions/setup-node@v5 with: node-version: ${{ inputs.node_version }} cache: ${{ inputs.package_manager }}
- name: Install dependencies run: | case "${{ inputs.package_manager }}" in pnpm) pnpm install --frozen-lockfile ;; yarn) yarn install --frozen-lockfile ;; *) npm ci ;; esac
- name: Download compiled artifacts if: inputs.artifact_name != '' uses: actions/download-artifact@v5 with: name: ${{ inputs.artifact_name }} path: .
# ============================================ # RUN TESTS # ============================================ - name: Run hardhat test id: test-run env: REPORT_GAS: ${{ inputs.run_gas_reporter }} run: | case "${{ inputs.package_manager }}" in pnpm) pnpm hardhat test 2>&1 | tee test-output.log || true ;; yarn) yarn hardhat test 2>&1 | tee test-output.log || true ;; *) npx hardhat test 2>&1 | tee test-output.log || true ;; esac
- name: Parse test results id: test-summary if: always() run: | PASSED=0 FAILED=0
if [ -f "test-output.log" ]; then # Mocha output: "X passing" and "Y failing" PASSED=$(grep -oE '[0-9]+ passing' test-output.log | grep -oE '[0-9]+' | tail -1 || echo "0") FAILED=$(grep -oE '[0-9]+ failing' test-output.log | grep -oE '[0-9]+' | tail -1 || echo "0")
if [ -z "$PASSED" ]; then PASSED=0; fi if [ -z "$FAILED" ]; then FAILED=0; fi fi
echo "passed=$PASSED" >> $GITHUB_OUTPUT echo "failed=$FAILED" >> $GITHUB_OUTPUT
TOTAL=$((PASSED + FAILED)) echo "### Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Passed | $PASSED |" >> $GITHUB_STEP_SUMMARY echo "| Failed | $FAILED |" >> $GITHUB_STEP_SUMMARY echo "| **Total** | **$TOTAL** |" >> $GITHUB_STEP_SUMMARY
# ============================================ # COVERAGE (solidity-coverage) # ============================================ - name: Run solidity-coverage if: inputs.run_coverage run: | case "${{ inputs.package_manager }}" in pnpm) pnpm hardhat coverage 2>&1 | tee coverage-output.log || true ;; yarn) yarn hardhat coverage 2>&1 | tee coverage-output.log || true ;; *) npx hardhat coverage 2>&1 | tee coverage-output.log || true ;; esac
- name: Parse coverage report id: coverage if: inputs.run_coverage && always() run: | COVERAGE=0
if [ -f "coverage/coverage-summary.json" ]; then COVERAGE=$(node -e " const report = require('./coverage/coverage-summary.json'); const pct = report.total.lines.pct; console.log(typeof pct === 'number' ? pct.toFixed(1) : '0'); " 2>/dev/null || echo "0") fi
echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT
echo "" >> $GITHUB_STEP_SUMMARY echo "### Code Coverage (Solidity)" >> $GITHUB_STEP_SUMMARY echo "**Line Coverage: ${COVERAGE}%**" >> $GITHUB_STEP_SUMMARY
- name: Check coverage threshold if: inputs.run_coverage && inputs.coverage_threshold > 0 && always() run: | COVERAGE="${{ steps.coverage.outputs.percentage }}" THRESHOLD="${{ inputs.coverage_threshold }}"
if [ "$(echo "$COVERAGE < $THRESHOLD" | bc -l)" -eq 1 ]; then echo "::error::Coverage ${COVERAGE}% is below threshold of ${THRESHOLD}%" exit 1 fi
echo "Coverage ${COVERAGE}% meets threshold of ${THRESHOLD}%"
# ============================================ # FAIL JOB ON TEST FAILURES # ============================================ - name: Check test results if: always() run: | FAILED="${{ steps.test-summary.outputs.failed }}" if [ -n "$FAILED" ] && [ "$FAILED" -gt 0 ]; then echo "::error::$FAILED test(s) failed" exit 1 fi
# ============================================ # UPLOAD REPORTS # ============================================ - name: Upload coverage reports if: inputs.upload_reports && inputs.run_coverage && always() uses: actions/upload-artifact@v5 with: name: contracts-coverage-${{ github.run_id }} path: coverage/ retention-days: 7 if-no-files-found: warn
- name: Upload gas report if: inputs.upload_reports && inputs.run_gas_reporter && always() uses: actions/upload-artifact@v5 with: name: contracts-gas-report-${{ github.run_id }} path: | gas-report.txt test-output.log retention-days: 7 if-no-files-found: warn Contracts (Hardhat/Solidity)· Reusable workflow ·on: workflow_call
Contracts Test
Contracts - Test & Coverage
.github/workflows/contracts-test.yml