Skip to content

Commit d12d64d

Browse files
authored
Change PR build to generate coverage and comment it (apple#1474)
This extends the github actions to by default run the coverage generation targets. The targets will generate the coverage artifacts, which will then be used by a seprate github action to comment on the PR the coverage numbers. This is a sanity check for testing information for new features and refactors alike. It will allow reviewers to quickly identify if there are major issues with new tests, or large changes to coverage percentage that indicate a problem.
1 parent 2ee3f3d commit d12d64d

4 files changed

Lines changed: 150 additions & 3 deletions

File tree

.github/workflows/common.yml

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ on:
1010
type: boolean
1111
description: "Publish this build for release"
1212
default: false
13+
coverage:
14+
type: boolean
15+
description: "Run tests with code coverage enabled"
16+
default: false
17+
pr_number:
18+
type: string
19+
description: "PR number for coverage artifact naming (required when coverage is true)"
20+
default: ""
1321

1422
jobs:
1523
buildAndTest:
@@ -64,13 +72,23 @@ jobs:
6472
env:
6573
DEVELOPER_DIR: "/Applications/Xcode-latest.app/Contents/Developer"
6674

75+
- name: Validate coverage inputs
76+
if: ${{ inputs.coverage }}
77+
env:
78+
PR_NUMBER: ${{ inputs.pr_number }}
79+
run: |
80+
if [ -z "${PR_NUMBER}" ]; then
81+
echo "::error::pr_number input is required when coverage is true"
82+
exit 1
83+
fi
84+
6785
- name: Create package
6886
run: |
6987
mkdir -p outputs
7088
mv "bin/${BUILD_CONFIGURATION}/container-installer-unsigned.pkg" outputs
7189
mv "bin/${BUILD_CONFIGURATION}/bundle/container-dSYM.zip" outputs
7290
73-
- name: Test the container project
91+
- name: Set up test environment
7492
run: |
7593
APP_ROOT=$(mktemp -d -p "${RUNNER_TEMP}")
7694
LOG_ROOT="${APP_ROOT}/logs"
@@ -82,10 +100,53 @@ jobs:
82100
echo no_proxy=${no_proxy}
83101
echo "APP_ROOT=${APP_ROOT}" >> $GITHUB_ENV
84102
echo "LOG_ROOT=${LOG_ROOT}" >> $GITHUB_ENV
85-
make APP_ROOT="${APP_ROOT}" LOG_ROOT="${LOG_ROOT}" test install-kernel integration
103+
104+
- name: Test the container project
105+
if: ${{ !inputs.coverage }}
106+
run: make APP_ROOT="${APP_ROOT}" LOG_ROOT="${LOG_ROOT}" test install-kernel integration
107+
env:
108+
DEVELOPER_DIR: "/Applications/Xcode-latest.app/Contents/Developer"
109+
110+
- name: Test the container project with coverage
111+
if: ${{ inputs.coverage }}
112+
run: make APP_ROOT="${APP_ROOT}" LOG_ROOT="${LOG_ROOT}" install-kernel coverage
86113
env:
87114
DEVELOPER_DIR: "/Applications/Xcode-latest.app/Contents/Developer"
88115

116+
- name: Extract coverage percentages
117+
if: ${{ inputs.coverage }}
118+
env:
119+
PR_NUMBER: ${{ inputs.pr_number }}
120+
run: |
121+
mkdir -p pr-coverage
122+
jq -r '.data[0].totals.lines.percent | . * 100 | round | . / 100' coverage-reports/unit/coverage-summary.json > pr-coverage/unit-line-coverage.txt
123+
jq -r '.data[0].totals.lines.percent | . * 100 | round | . / 100' coverage-reports/integration/coverage-summary.json > pr-coverage/integration-line-coverage.txt
124+
jq -r '.data[0].totals.lines.percent | . * 100 | round | . / 100' coverage-reports/combined/coverage-summary.json > pr-coverage/combined-line-coverage.txt
125+
echo "${PR_NUMBER}" > pr-coverage/pr-number.txt
126+
echo "Coverage data:"
127+
cat pr-coverage/*.txt
128+
129+
- name: Upload coverage data
130+
if: ${{ inputs.coverage }}
131+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
132+
with:
133+
name: pr-coverage-${{ inputs.pr_number }}
134+
path: pr-coverage/
135+
retention-days: 1
136+
if-no-files-found: warn
137+
138+
- name: Upload coverage HTML reports
139+
if: ${{ inputs.coverage }}
140+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
141+
with:
142+
name: coverage-html-reports
143+
path: |
144+
coverage-reports/unit/html/
145+
coverage-reports/integration/html/
146+
coverage-reports/combined/html/
147+
retention-days: 14
148+
if-no-files-found: ignore
149+
89150
- name: Archive test logs
90151
if: always()
91152
run: |
@@ -103,6 +164,7 @@ jobs:
103164
rm -rf "${APP_ROOT}"
104165
echo "Removed data directory ${APP_ROOT}"
105166
fi
167+
rm -rf coverage-reports pr-coverage
106168
107169
- name: Upload logs if present
108170
if: always()

.github/workflows/pr-build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ jobs:
4444
uses: ./.github/workflows/common.yml
4545
with:
4646
release: false
47+
coverage: true
48+
pr_number: ${{ github.event.pull_request.number }}
4749
secrets: inherit
4850
permissions:
4951
contents: read
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: PR Coverage Comment
2+
3+
on:
4+
workflow_run:
5+
workflows: ["container project - PR build"]
6+
types:
7+
- completed
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
comment:
14+
name: Post coverage comment
15+
runs-on: ubuntu-latest
16+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
17+
permissions:
18+
contents: read
19+
actions: read
20+
pull-requests: write
21+
22+
steps:
23+
- name: Download coverage artifact
24+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
25+
with:
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
run-id: ${{ github.event.workflow_run.id }}
28+
pattern: pr-coverage-*
29+
merge-multiple: false
30+
id: download-artifact
31+
continue-on-error: true
32+
33+
- name: Check artifact exists
34+
if: steps.download-artifact.outcome == 'success'
35+
id: check-artifact
36+
run: |
37+
if ls pr-coverage-*/pr-number.txt 1>/dev/null 2>&1; then
38+
echo "found=true" >> $GITHUB_OUTPUT
39+
else
40+
echo "No coverage artifact found — coverage job may have failed."
41+
echo "found=false" >> $GITHUB_OUTPUT
42+
fi
43+
44+
- name: Validate and post comment
45+
if: steps.check-artifact.outputs.found == 'true'
46+
env:
47+
GH_TOKEN: ${{ github.token }}
48+
REPO: ${{ github.repository }}
49+
run: |
50+
PR_NUMBER=$(cat pr-coverage-*/pr-number.txt)
51+
grep -qxE '[0-9]+' <<< "$PR_NUMBER" || { echo "Invalid PR number: ${PR_NUMBER}"; exit 1; }
52+
53+
UNIT=$(cat pr-coverage-*/unit-line-coverage.txt)
54+
grep -qxE '[0-9]+(\.[0-9]+)?' <<< "$UNIT" || { echo "Invalid unit coverage: ${UNIT}"; exit 1; }
55+
56+
INTEGRATION=$(cat pr-coverage-*/integration-line-coverage.txt)
57+
grep -qxE '[0-9]+(\.[0-9]+)?' <<< "$INTEGRATION" || { echo "Invalid integration coverage: ${INTEGRATION}"; exit 1; }
58+
59+
COMBINED=$(cat pr-coverage-*/combined-line-coverage.txt)
60+
grep -qxE '[0-9]+(\.[0-9]+)?' <<< "$COMBINED" || { echo "Invalid combined coverage: ${COMBINED}"; exit 1; }
61+
62+
MARKER="<!-- coverage-bot -->"
63+
BODY=$(cat <<EOF
64+
${MARKER}
65+
## Code Coverage
66+
67+
| Tier | Line Coverage |
68+
|------|--------------|
69+
| Unit | ${UNIT}% |
70+
| Integration | ${INTEGRATION}% |
71+
| Combined | ${COMBINED}% |
72+
EOF
73+
)
74+
75+
EXISTING_COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -1)
76+
77+
if [ -n "${EXISTING_COMMENT_ID}" ]; then
78+
gh api "repos/${REPO}/issues/comments/${EXISTING_COMMENT_ID}" -X PATCH -f body="${BODY}"
79+
echo "Updated existing coverage comment ${EXISTING_COMMENT_ID}"
80+
else
81+
gh pr comment "${PR_NUMBER}" --repo "${REPO}" --body "${BODY}"
82+
echo "Created new coverage comment"
83+
fi

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ define GENERATE_COV_REPORTS
182182
-output-dir=$(COVERAGE_OUTPUT_DIR)/$(2)/html \
183183
$(TEST_BINARY)
184184
@echo Extracting $(2) coverage percentages...
185-
@jq -r '"line coverage: \(.data[0].totals.lines.percent * 100 | round / 100)%\nfunction coverage: \(.data[0].totals.functions.percent * 100 | round / 100)%"' \
185+
@jq -r '"line coverage: \(.data[0].totals.lines.percent | . * 100 | round | . / 100)%\nfunction coverage: \(.data[0].totals.functions.percent | . * 100 | round | . / 100)%"' \
186186
$(COVERAGE_OUTPUT_DIR)/$(2)/coverage-summary.json > $(COVERAGE_OUTPUT_DIR)/$(2)/coverage-percent.txt
187187
@cat $(COVERAGE_OUTPUT_DIR)/$(2)/coverage-percent.txt
188188
endef

0 commit comments

Comments
 (0)