Skip to content

Combined Coverage Artifact #2132

Combined Coverage Artifact

Combined Coverage Artifact #2132

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