Skip to content

wwuoneway running code coverage #4400

wwuoneway running code coverage

wwuoneway running code coverage #4400

Workflow file for this run

# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Code Coverage
run-name: "${{ github.actor }} running code coverage"
"on":
push:
branches: [main, "release/*"]
pull_request:
issue_comment:
types: [created]
workflow_dispatch:
inputs:
ref:
description: "The branch, ref, or SHA to checkout. Defaults to the repository's default branch."
required: false
type: string
repo:
description: "The repository to checkout. Defaults to the current repository."
required: false
type: string
pr-base-sha:
description: "The base SHA for diffing (PR base). Defaults to event-provided value."
required: false
type: string
phlex-coverage-compiler:
description: "Compiler to use for coverage build (gcc or clang)"
required: false
default: "clang"
phlex-enable-form:
description: "Enable FORM integration (set to OFF to exclude FORM sources)"
required: false
default: "ON"
permissions:
contents: read
pull-requests: read
jobs:
setup:
if: >
github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'push' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(fromJSON('["OWNER", "COLLABORATOR", "MEMBER"]'), github.event.comment.author_association) &&
startsWith(github.event.comment.body, format('@{0}bot coverage', github.event.repository.name))
)
runs-on: ubuntu-latest
outputs:
is_act: ${{ steps.setup.outputs.is_act }}
ref: ${{ steps.setup.outputs.ref }}
repo: ${{ steps.setup.outputs.repo }}
base_sha: ${{ steps.setup.outputs.base_sha }}
pr_number: ${{ steps.setup.outputs.pr_number }}
checkout_path: ${{ steps.setup.outputs.checkout_path }}
build_path: ${{ steps.setup.outputs.build_path }}
has_changes: ${{ steps.setup.outputs.has_changes }}
steps:
- name: Workflow setup
id: setup
uses: Framework-R-D/action-workflow-setup@f73307dd8c13cb66c2565c9ace32571517b1cea8 # v1
with:
ref: ${{ inputs.ref }}
repo: ${{ inputs.repo }}
pr-base-sha: ${{ inputs.pr-base-sha }}
file-type: |
cpp
cmake
python
exclude-globs: scripts/**
coverage:
needs: setup
if: >
always() && (github.event_name == 'workflow_dispatch' || needs.setup.outputs.has_changes == 'true')
runs-on: ubuntu-24.04
container:
image: ghcr.io/framework-r-d/phlex-ci:latest
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
outputs:
has_coverage_xml: ${{ steps.coverage_outputs.outputs.has_coverage_xml }}
has_coverage_html: ${{ steps.coverage_outputs.outputs.has_coverage_html }}
has_coverage_llvm_info: ${{ steps.coverage_outputs.outputs.has_coverage_llvm_info }}
has_coverage_python_xml: ${{ steps.coverage_outputs.outputs.has_coverage_python_xml }}
artifact_upload_available: ${{ steps.artifact_runtime.outputs.available }}
steps:
- name: Determine coverage options
id: coverage_options
shell: bash
# yamllint disable rule:line-length
env:
REQUESTED_COMPILER:
${{ github.event_name == 'workflow_dispatch' && github.event.inputs['phlex-coverage-compiler'] || '' }}
REQUESTED_FORM:
${{ github.event_name == 'workflow_dispatch' && github.event.inputs['phlex-enable-form'] || '' }}
run: |
set -euo pipefail
compiler="${REQUESTED_COMPILER:-clang}"
form="${REQUESTED_FORM:-ON}"
echo "compiler=$compiler" >> "$GITHUB_OUTPUT"
echo "enable_form=$form" >> "$GITHUB_OUTPUT"
# yamllint enable
- name: Check out source code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref:
${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || needs.setup.outputs.ref }}
path: ${{ needs.setup.outputs.checkout_path }}
repository:
${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name ||
needs.setup.outputs.repo }}
persist-credentials: false
- name: Setup build environment
uses: Framework-R-D/action-setup-build-env@780fd75b00a291df6a42d9d2337651330ebdb907 # v1
with:
build-path: ${{ needs.setup.outputs.build_path }}
- name: Announce CMake configuration
run: echo "➡️ Configuring CMake for coverage..."
- name: Configure CMake with GCC coverage
id: configure_gcc
if: ${{ steps.coverage_options.outputs.compiler == 'gcc' }}
uses: Framework-R-D/action-configure-cmake@caba9b42770d8f3ad2d8f294ef82dd88fc2c10c6 # v2
with:
cpp-compiler: g++
preset: coverage-gcc
source-path: ${{ needs.setup.outputs.checkout_path }}
build-path: ${{ needs.setup.outputs.build_path }}
enable-form: ${{ steps.coverage_options.outputs.enable_form }}
form-root-storage: ${{ steps.coverage_options.outputs.enable_form }}
- name: Configure CMake with Clang coverage
id: configure_clang
if: ${{ steps.coverage_options.outputs.compiler == 'clang' }}
uses: Framework-R-D/action-configure-cmake@caba9b42770d8f3ad2d8f294ef82dd88fc2c10c6 # v2
with:
cpp-compiler: clang++
preset: coverage-clang
source-path: ${{ needs.setup.outputs.checkout_path }}
build-path: ${{ needs.setup.outputs.build_path }}
enable-form: ${{ steps.coverage_options.outputs.enable_form }}
form-root-storage: ${{ steps.coverage_options.outputs.enable_form }}
- name: Unknown Compiler Error
if:
${{ steps.coverage_options.outputs.compiler != 'gcc' && steps.coverage_options.outputs.compiler != 'clang' }}
env:
COMPILER: ${{ steps.coverage_options.outputs.compiler }}
run: |
echo "ERROR: Unknown compiler '${COMPILER}'. Must be 'gcc' or 'clang'."
exit 1
- name: Announce build
run: echo "➡️ Building with coverage instrumentation..."
- name: Build with coverage instrumentation
id: build
uses: Framework-R-D/action-build-cmake@30bca0b2d76e90cf1cc9e6e863aed5be505e7e61 # v1
with:
build-path: ${{ needs.setup.outputs.build_path }}
- name: Run tests with coverage
working-directory: ${{ needs.setup.outputs.build_path }}
run: |
. /entrypoint.sh
echo "➡️ Running tests with coverage..."
PROFILE_ROOT="$(pwd)/test/profraw"
echo "Cleaning LLVM profile directory: $PROFILE_ROOT"
rm -rf "$PROFILE_ROOT"
mkdir -p "$PROFILE_ROOT"
export LLVM_PROFILE_FILE="$PROFILE_ROOT/%m-%p.profraw"
echo "::group::Running ctest for coverage"
if ctest --progress --output-on-failure -j "$(nproc)"; then
echo "::endgroup::"
echo "✅ All tests passed."
else
echo "::endgroup::"
echo "::error::Some tests failed."
exit 1
fi
- name: Generate coverage reports (GCC)
id: report_gcc
if: ${{ steps.coverage_options.outputs.compiler == 'gcc' }}
shell: bash
working-directory: ${{ needs.setup.outputs.build_path }}
run: |
. /entrypoint.sh
echo "➡️ Generating coverage reports for GCC..."
echo "::group::Running coverage-gcov target"
if cmake --build . --target coverage-gcov -v; then
echo "::endgroup::"
echo "✅ GCC coverage report generation succeeded."
else
echo "::endgroup::"
echo "::error::GCC coverage report generation failed."
exit 1
fi
- name: Generate coverage reports (Clang)
id: report_clang
if: ${{ steps.coverage_options.outputs.compiler == 'clang' }}
shell: bash
working-directory: ${{ needs.setup.outputs.build_path }}
run: |
. /entrypoint.sh
echo "➡️ Generating coverage reports for Clang..."
echo "::group::Running coverage-llvm target"
if cmake --build . --target coverage-llvm -v; then
echo "::endgroup::"
echo "✅ Clang coverage report generation succeeded."
else
echo "::endgroup::"
echo "::error::Clang coverage report generation failed."
exit 1
fi
- name: Generate Python coverage report
id: report_python
if: ${{ steps.report_gcc.outcome != 'skipped' || steps.report_clang.outcome != 'skipped' }}
shell: bash
working-directory: ${{ needs.setup.outputs.build_path }}
run: |
. /entrypoint.sh
echo "➡️ Generating Python coverage report..."
echo "::group::Running coverage-python target"
# Check if pytest-cov is available
if python3 -c "import pytest_cov" 2>/dev/null; then
if cmake --build . --target coverage-python -v; then
echo "::endgroup::"
echo "✅ Python coverage report generation succeeded."
else
echo "::endgroup::"
echo "::warning::Python coverage report generation failed (non-fatal)."
fi
else
echo "::endgroup::"
echo "::notice::pytest-cov not available; skipping Python coverage report."
fi
- name: Generate scripts coverage report
id: report_scripts
if: ${{ steps.report_gcc.outcome != 'skipped' || steps.report_clang.outcome != 'skipped' }}
shell: bash
env:
SOURCE_PATH: ${{ needs.setup.outputs.checkout_path }}
BUILD_PATH: ${{ needs.setup.outputs.build_path }}
run: |
. /entrypoint.sh
SOURCE_DIR="$GITHUB_WORKSPACE/$SOURCE_PATH"
BUILD_DIR="$GITHUB_WORKSPACE/$BUILD_PATH"
echo "➡️ Generating scripts/ Python coverage report..."
if ! python3 -c "import pytest_cov" 2>/dev/null; then
echo "::notice::pytest-cov not available; skipping scripts coverage report."
exit 0
fi
cd "$SOURCE_DIR"
if python3 -m pytest \
scripts/test/ \
--cov=scripts \
--cov-report=xml:"$BUILD_DIR/coverage-scripts.xml" \
--cov-report=term-missing \
-q; then
echo "✅ Scripts coverage report generation succeeded."
else
echo "::warning::Scripts coverage report generation failed (non-fatal)."
fi
- name: Capture coverage artifact availability
id: coverage_outputs
if: ${{ steps.report_gcc.outcome != 'skipped' || steps.report_clang.outcome != 'skipped' }}
shell: bash
working-directory: ${{ needs.setup.outputs.build_path }}
run: |
if [ -f coverage.xml ]; then
echo "has_coverage_xml=true" >> "$GITHUB_OUTPUT"
else
echo "has_coverage_xml=false" >> "$GITHUB_OUTPUT"
fi
if [ -d coverage-html ]; then
echo "has_coverage_html=true" >> "$GITHUB_OUTPUT"
else
echo "has_coverage_html=false" >> "$GITHUB_OUTPUT"
fi
if [ -f coverage-llvm.info ]; then
echo "has_coverage_llvm_info=true" >> "$GITHUB_OUTPUT"
else
echo "has_coverage_llvm_info=false" >> "$GITHUB_OUTPUT"
fi
if [ -f coverage-python.xml ]; then
echo "has_coverage_python_xml=true" >> "$GITHUB_OUTPUT"
else
echo "has_coverage_python_xml=false" >> "$GITHUB_OUTPUT"
fi
- name: Unknown Coverage Report Error
if: ${{ steps.report_gcc.outcome == 'skipped' && steps.report_clang.outcome == 'skipped' }}
run: |
echo "ERROR: No coverage report was generated. Must run either GCC or Clang coverage report step."
exit 1
- name: Prepare coverage artifact bundle
if: ${{ steps.report_gcc.outcome != 'skipped' || steps.report_clang.outcome != 'skipped' }}
shell: bash
working-directory: ${{ needs.setup.outputs.build_path }}
run: |
set -euo pipefail
ARTIFACT_DIR="$GITHUB_WORKSPACE/coverage-artifacts"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
for file in \
coverage-llvm.txt \
coverage-llvm.info \
coverage.profdata \
coverage.xml \
coverage.info \
coverage.info.final \
coverage-python.xml \
coverage-scripts.xml; do
if [ -f "$file" ]; then
cp "$file" "$ARTIFACT_DIR/"
fi
done
if [ -d coverage-html ]; then cp -R coverage-html "$ARTIFACT_DIR/"; fi
if [ -d coverage-python-html ]; then cp -R coverage-python-html "$ARTIFACT_DIR/"; fi
- name: Detect artifact upload availability
id: artifact_runtime
if: ${{ steps.report_gcc.outcome != 'skipped' || steps.report_clang.outcome != 'skipped' }}
shell: bash
# yamllint disable rule:line-length
run: |
if [ "${GITHUB_ACTOR}" = "nektos/act" ] || [ "${ACT:-}" = "true" ]; then
echo "::notice::Artifact upload disabled for local act executions."
echo "available=false" >> "$GITHUB_OUTPUT"
elif [ -z "${ACTIONS_RUNTIME_TOKEN:-}" ]; then
echo "::warning::ACTIONS_RUNTIME_TOKEN missing; continuing and trusting actions/upload-artifact to handle authentication."
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=true" >> "$GITHUB_OUTPUT"
fi
# yamllint enable
- name: Install Node.js runtime for artifact publishing
if:
${{ (steps.report_gcc.outcome != 'skipped' || steps.report_clang.outcome != 'skipped') &&
steps.artifact_runtime.outputs.available == 'true' && github.actor != 'nektos/act' }}
run: |
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get update
apt-get install -y nodejs
- name: Upload coverage bundle
if:
${{ (steps.report_gcc.outcome != 'skipped' || steps.report_clang.outcome != 'skipped') &&
steps.artifact_runtime.outputs.available == 'true' && github.actor != 'nektos/act' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-generated
path: coverage-artifacts/
retention-days: 30
coverage-upload:
needs: [setup, coverage]
if:
${{ needs.coverage.result == 'success' && needs.coverage.outputs.artifact_upload_available == 'true' &&
github.actor != 'nektos/act' }}
runs-on: ubuntu-latest
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
steps:
- name: Check out source code for Codecov mapping
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref:
${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || needs.setup.outputs.ref }}
repository:
${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name ||
needs.setup.outputs.repo }}
fetch-depth: 0
persist-credentials: false
- name: Download coverage bundle
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: coverage-generated
path: coverage-artifacts
- name: Determine coverage files for Codecov
id: codecov_targets
run: |
set -euo pipefail
files=()
if [ -f coverage-artifacts/coverage.xml ]; then
files+=("coverage-artifacts/coverage.xml")
fi
if [ -f coverage-artifacts/coverage-llvm.info ]; then
files+=("coverage-artifacts/coverage-llvm.info")
fi
if [ -f coverage-artifacts/coverage-python.xml ]; then
files+=("coverage-artifacts/coverage-python.xml")
fi
ls -al coverage-artifacts || true
if [ "${#files[@]}" -eq 0 ]; then
echo "No coverage files detected; skipping Codecov upload." >&2
echo "found=false" >> "$GITHUB_OUTPUT"
echo "files=" >> "$GITHUB_OUTPUT"
else
file_csv=$(IFS=,; printf '%s' "${files[*]}")
echo "Codecov upload targets: ${files[*]}"
echo "found=true" >> "$GITHUB_OUTPUT"
echo "files=${file_csv}" >> "$GITHUB_OUTPUT"
fi
- name: Upload coverage to Codecov
if: ${{ steps.codecov_targets.outputs.found == 'true' }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ${{ steps.codecov_targets.outputs.files }}
flags: unittests
name: phlex-coverage
fail_ci_if_error: true
verbose: true
root_dir: .
token: ${{ env.CODECOV_TOKEN }}
- name: Upload scripts coverage to Codecov
if: ${{ hashFiles('coverage-artifacts/coverage-scripts.xml') != '' }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: coverage-artifacts/coverage-scripts.xml
flags: scripts
name: phlex-scripts-coverage
# Non-fatal: scripts coverage is informational (no CI threshold yet).
fail_ci_if_error: false
verbose: true
root_dir: .
token: ${{ env.CODECOV_TOKEN }}
- name: Publish HTML coverage report
if: ${{ needs.coverage.outputs.has_coverage_html == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-html-report
path: coverage-artifacts/coverage-html/
if-no-files-found: warn
retention-days: 30
coverage-complete:
needs: [setup, coverage, coverage-upload]
if: always() && github.event_name == 'issue_comment' && needs.setup.result == 'success'
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Cleanup old coverage comments
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ github.event.issue.number || '' }}
MARKER: "<!-- coverage-comment -->"
with:
script: |
const marker = process.env.MARKER;
const pr = parseInt(process.env.PR_NUMBER, 10);
if (isNaN(pr)) { console.log('No PR number, skipping cleanup'); process.exit(0); }
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr
});
for (const c of comments) {
if (c.body && c.body.includes(marker)) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: c.id
});
console.log(`Deleted old comment ${c.id}`);
}
}
- name: Comment on coverage completion
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
with:
message: |
Coverage workflow completed.
**Result:** ${{ needs.coverage.result == 'success' && '✅ Coverage analysis passed.'
|| needs.coverage.result == 'failure' && '❌ Coverage analysis failed.'
|| needs.coverage.result == 'cancelled' && '⚠️ Coverage analysis was cancelled.'
|| needs.coverage.result == 'skipped' && 'ℹ️ No relevant changes detected; coverage skipped.'
|| format('ℹ️ Coverage completed with status: {0}.', needs.coverage.result) }}
See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for detailed results.
<!-- coverage-comment -->
- name: Update PR comment reactions
if: always()
uses: Framework-R-D/action-complete-pr-comment@5d16749c32fbb7b15503f264dde6e3c98b90b4fb # v2
with:
status: ${{ needs.coverage.result }}