Combined Coverage Artifact #2132
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: Combined Coverage Artifact | |
| on: | |
| workflow_run: | |
| workflows: ["Integration Tests"] | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| actions: read | |
| concurrency: | |
| group: "coverage-report" | |
| cancel-in-progress: true | |
| jobs: | |
| combine: | |
| runs-on: ubuntu-latest | |
| # A combined report is authoritative only for a successful complete matrix. | |
| if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} | |
| - name: Setup Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install coverage tools | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install coverage[toml] genbadge[coverage] | |
| - name: Download all coverage artifacts from triggering workflow | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| // Debug: Log the workflow_run context | |
| console.log('Event name:', context.eventName); | |
| console.log('Workflow run ID:', context.payload.workflow_run?.id || 'N/A'); | |
| console.log('Workflow run conclusion:', context.payload.workflow_run?.conclusion || 'N/A'); | |
| // Determine the run ID to use | |
| let runId; | |
| if (context.eventName === 'workflow_run') { | |
| runId = context.payload.workflow_run.id; | |
| console.log('Using workflow_run trigger, run ID:', runId); | |
| } else if (context.eventName === 'workflow_dispatch') { | |
| // Manual reports must use the same commit that was checked out. | |
| const runs = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'integration-tests.yml', | |
| per_page: 100, | |
| status: 'success' | |
| }); | |
| const matchingRun = runs.data.workflow_runs.find( | |
| run => run.head_sha === context.sha | |
| ); | |
| if (matchingRun === undefined) { | |
| throw new Error(`No successful integration test run found for ${context.sha}`); | |
| } | |
| runId = matchingRun.id; | |
| console.log('Using workflow_dispatch trigger, matching run ID:', runId); | |
| } else { | |
| throw new Error(`Unsupported event type: ${context.eventName}`); | |
| } | |
| // Get all artifacts from the workflow run | |
| const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: runId, | |
| per_page: 100, | |
| }); | |
| console.log(`Found ${artifacts.length} total artifacts`); | |
| // Filter for coverage artifacts | |
| const coverageArtifacts = artifacts.filter(a => a.name.startsWith('coverage-')); | |
| console.log(`Found ${coverageArtifacts.length} coverage artifacts:`, coverageArtifacts.map(a => a.name)); | |
| if (coverageArtifacts.length === 0) { | |
| throw new Error(`Integration workflow ${runId} produced no coverage artifacts`); | |
| } | |
| // Create coverage-reports directory | |
| const coverageDir = 'coverage-reports'; | |
| if (!fs.existsSync(coverageDir)) { | |
| fs.mkdirSync(coverageDir, { recursive: true }); | |
| } | |
| // Download each coverage artifact | |
| for (const artifact of coverageArtifacts) { | |
| console.log(`Downloading artifact: ${artifact.name}`); | |
| const download = await github.rest.actions.downloadArtifact({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| artifact_id: artifact.id, | |
| archive_format: 'zip', | |
| }); | |
| // Save the zip file | |
| const zipPath = path.join(coverageDir, `${artifact.name}.zip`); | |
| fs.writeFileSync(zipPath, Buffer.from(download.data)); | |
| // Unzip the artifact | |
| const artifactDir = path.join(coverageDir, artifact.name); | |
| if (!fs.existsSync(artifactDir)) { | |
| fs.mkdirSync(artifactDir, { recursive: true }); | |
| } | |
| // Use unzip command to extract | |
| const { execSync } = require('child_process'); | |
| execSync(`unzip -q "${zipPath}" -d "${artifactDir}"`); | |
| // Remove the zip file | |
| fs.unlinkSync(zipPath); | |
| console.log(`Extracted ${artifact.name} to ${artifactDir}`); | |
| } | |
| - name: Combine coverage reports | |
| run: | | |
| mkdir -p site | |
| mapfile -d '' coverage_files < <( | |
| find coverage-reports -name ".coverage*" -type f -print0 | |
| ) | |
| if [ "${#coverage_files[@]}" -eq 0 ]; then | |
| echo "Coverage artifacts contained no coverage data files" >&2 | |
| exit 1 | |
| fi | |
| printf 'Combining %s coverage data files\n' "${#coverage_files[@]}" | |
| coverage combine --keep "${coverage_files[@]}" | |
| # Generate combined reports | |
| # This is an integrity floor, not a project coverage target. It catches | |
| # empty and pre-imported test runs before publishing a misleading page. | |
| coverage report --fail-under=1 | |
| coverage xml -o coverage.xml | |
| coverage html -d site/coverage | |
| echo "=== Combined coverage report generated ===" | |
| ls -la coverage.xml site/coverage/ | |
| - name: Generate coverage badge | |
| run: | | |
| mkdir -p .github/badges | |
| genbadge coverage -i coverage.xml -o .github/badges/coverage.svg -n "coverage report" | |
| - name: Create index.html and README | |
| run: | | |
| cat > site/index.html << 'EOF' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta http-equiv="refresh" content="0; url=./coverage/"> | |
| <title>Redirecting to coverage report...</title> | |
| </head> | |
| <body> | |
| <p>Redirecting to coverage report... <a href="./coverage/">Click here if not redirected</a></p> | |
| </body> | |
| </html> | |
| EOF | |
| # Create README.md in the site directory | |
| cat > site/README.md << 'EOF' | |
| # OpenHCS Code Coverage Reports | |
| This site contains the combined code coverage reports for the [OpenHCS](https://github.com/OpenHCSDev/OpenHCS) project. | |
| ## Navigation | |
| - [Coverage Report](./coverage/): View the HTML coverage report | |
| ## About | |
| These reports are automatically generated by GitHub Actions and combine coverage from the integration test jobs: | |
| - Foundational unit and core tests | |
| - Maintained PyQt GUI tests | |
| - Python boundary tests (3.11, 3.13) across Linux, Windows, macOS | |
| - Backend/microscope combinations (disk, zarr × ImageXpress, OperaPhenix) across all OSes | |
| - OMERO tests (Linux only, Python 3.11-3.12) | |
| - Wheel installation tests | |
| The combined coverage shows the code exercised by these instrumented suites together. | |
| EOF | |
| generated_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ') | |
| printf '\nLast updated: %s\n' "$generated_at" >> site/README.md | |
| # Disabled: Auto-committing badges causes push conflicts | |
| # - name: Commit and push if coverage badge changed | |
| # if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| # run: | | |
| # git config --local user.email "github-actions[bot]@users.noreply.github.com" | |
| # git config --local user.name "github-actions[bot]" | |
| # git add .github/badges/coverage.svg -f | |
| # git commit -m "chore: update coverage badge" || exit 0 | |
| # git push | |
| - name: Upload combined coverage report | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: combined-coverage-report | |
| path: | | |
| site | |
| coverage.xml | |
| .github/badges/coverage.svg | |
| if-no-files-found: error | |
| retention-days: 14 |