Skip to content

Commit 8923b91

Browse files
authored
add github release action (#22)
* disable smoke testing until district script migration is complete * modularize GitHub Actions workflows: separate build-wheel and release processes * add pytest-cov support and integrate coverage reporting in build and CI
1 parent f72bbac commit 8923b91

6 files changed

Lines changed: 219 additions & 27 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
"""Render a Cobertura-style coverage.xml (as produced by `coverage xml` /
3+
pytest-cov's --cov-report=xml) as Markdown suitable for a GitHub Actions job
4+
summary.
5+
6+
Usage:
7+
python3 python_coverage_summary.py <path-to-coverage.xml>
8+
9+
Prints to stdout; the caller is expected to redirect into $GITHUB_STEP_SUMMARY.
10+
Deliberately has no third-party dependencies so it can run with the stock
11+
`python3` already available on GitHub-hosted runners -- no extra permissions
12+
or installs are needed, which keeps it safe to run on pull requests from
13+
forks.
14+
"""
15+
16+
import sys
17+
import xml.etree.ElementTree as ET
18+
19+
20+
def _pct(rate_attr):
21+
try:
22+
return float(rate_attr) * 100
23+
except (TypeError, ValueError):
24+
return None
25+
26+
27+
def _fmt_pct(value):
28+
return "N/A" if value is None else f"{value:.1f}%"
29+
30+
31+
def main(argv):
32+
if len(argv) != 2:
33+
print("Usage: python_coverage_summary.py <coverage.xml>", file=sys.stderr)
34+
return 2
35+
36+
path = argv[1]
37+
38+
try:
39+
root = ET.parse(path).getroot()
40+
except (OSError, ET.ParseError) as exc:
41+
print("## Python test coverage")
42+
print()
43+
print(f"No coverage report found at `{path}` ({exc}).")
44+
return 0
45+
46+
line_rate = _pct(root.get("line-rate"))
47+
branch_rate = _pct(root.get("branch-rate"))
48+
lines_covered = root.get("lines-covered", "?")
49+
lines_valid = root.get("lines-valid", "?")
50+
51+
print("## Python test coverage")
52+
print()
53+
print(
54+
f"**Overall line coverage: {_fmt_pct(line_rate)}** "
55+
f"({lines_covered}/{lines_valid} lines) &nbsp;|&nbsp; "
56+
f"branch coverage: {_fmt_pct(branch_rate)}"
57+
)
58+
print()
59+
print("<details><summary>Per-file coverage</summary>")
60+
print()
61+
print("| File | Line coverage | Lines covered |")
62+
print("| --- | --- | --- |")
63+
64+
classes = sorted(root.iter("class"), key=lambda c: c.get("filename", ""))
65+
for cls in classes:
66+
filename = cls.get("filename", "?")
67+
file_line_rate = _pct(cls.get("line-rate"))
68+
69+
total = covered = 0
70+
lines_elem = cls.find("lines")
71+
if lines_elem is not None:
72+
for line in lines_elem.findall("line"):
73+
total += 1
74+
if int(line.get("hits", "0")) > 0:
75+
covered += 1
76+
77+
print(f"| `{filename}` | {_fmt_pct(file_line_rate)} | {covered}/{total} |")
78+
79+
print()
80+
print("</details>")
81+
82+
return 0
83+
84+
85+
if __name__ == "__main__":
86+
raise SystemExit(main(sys.argv))

.github/workflows/build-wheel.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Build Python Wheel
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
ref:
7+
description: Git ref to check out
8+
required: false
9+
type: string
10+
default: ""
11+
12+
jobs:
13+
build-wheel:
14+
name: Build Python wheel
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v7
20+
with:
21+
fetch-depth: 0
22+
ref: ${{ inputs.ref || github.ref }}
23+
24+
- name: Set up Java
25+
uses: actions/setup-java@v5
26+
with:
27+
distribution: temurin
28+
java-version: 21
29+
30+
- name: Set up Gradle
31+
uses: gradle/actions/setup-gradle@v6
32+
33+
- name: Make Gradle wrapper executable
34+
run: chmod +x ./gradlew
35+
36+
- name: Build and test
37+
run: ./gradlew clean build buildPythonWheel
38+
39+
- name: Publish Python coverage summary
40+
if: always()
41+
run: |
42+
python3 .github/scripts/python_coverage_summary.py \
43+
regi-headless/build/reports/coverage/coverage.xml \
44+
>> "$GITHUB_STEP_SUMMARY"
45+
46+
- name: Upload Python coverage report
47+
if: always()
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: python-coverage-html
51+
path: regi-headless/build/reports/coverage/html
52+
if-no-files-found: warn
53+
retention-days: 14
54+
55+
- name: Collect wheel
56+
run: |
57+
mkdir -p dist
58+
find . -path "*/build/install/*/dist/*.whl" -exec cp {} dist/ \;
59+
60+
- name: Upload Python wheel artifact
61+
uses: actions/upload-artifact@v4
62+
with:
63+
name: python-wheel
64+
path: dist/*.whl
65+
if-no-files-found: error
66+
retention-days: 14

.github/workflows/build.yml

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,18 @@ on:
99
permissions:
1010
contents: read
1111

12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
1216
jobs:
13-
build:
17+
build-wheel:
1418
name: Build and test
15-
runs-on: ubuntu-latest
16-
17-
steps:
18-
- name: Checkout repository
19-
uses: actions/checkout@v7
20-
with:
21-
fetch-depth: 0
22-
23-
- name: Set up Java
24-
uses: actions/setup-java@v5
25-
with:
26-
distribution: temurin
27-
java-version: 21
28-
29-
- name: Set up Gradle
30-
uses: gradle/actions/setup-gradle@v6
31-
32-
- name: Make Gradle wrapper executable
33-
run: chmod +x ./gradlew
34-
35-
- name: Build and test
36-
run: ./gradlew clean build
19+
uses: ./.github/workflows/build-wheel.yml
3720

3821
dependency-submission:
3922
name: Submit Gradle dependencies
40-
needs: build
23+
needs: build-wheel
4124
runs-on: ubuntu-latest
4225
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
4326
permissions:

.github/workflows/release.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types:
6+
- published
7+
8+
permissions:
9+
contents: read
10+
actions: read
11+
12+
concurrency:
13+
group: release-${{ github.event.release.id }}
14+
cancel-in-progress: false
15+
16+
jobs:
17+
build-wheel:
18+
name: Build wheel from release tag
19+
uses: ./.github/workflows/build-wheel.yml
20+
with:
21+
ref: ${{ github.event.release.tag_name }}
22+
23+
publish-wheel:
24+
name: Publish Python wheel to GitHub Release
25+
needs: build-wheel
26+
runs-on: ubuntu-latest
27+
permissions:
28+
contents: write
29+
30+
steps:
31+
- name: Download Python wheel artifact
32+
uses: actions/download-artifact@v4
33+
with:
34+
name: python-wheel
35+
path: dist
36+
37+
- name: Generate checksums
38+
run: |
39+
cd dist
40+
sha256sum *.whl > SHA256SUMS.txt
41+
42+
- name: Publish wheel to GitHub Release
43+
env:
44+
GH_TOKEN: ${{ github.token }}
45+
run: |
46+
gh release upload "${{ github.event.release.tag_name }}" dist/*.whl dist/SHA256SUMS.txt --clobber

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
*.iml
55
.DS_STORE
66
*/build/
7+
.coverage*

regi-headless/build.gradle

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ tasks.register('installPythonBuildTools', VenvTask) {
6060
description = 'Installs Python packages needed to build and test the wheel.'
6161

6262
venvExec = 'pip'
63-
args = ['install', '--upgrade', 'pip', 'build', 'pytest']
63+
args = ['install', '--upgrade', 'pip', 'build', 'pytest', 'pytest-cov']
6464

6565
outputs.file(layout.buildDirectory.file("python-build-tools/install.marker"))
6666

@@ -152,15 +152,25 @@ tasks.register('installPythonWheelForSmokeTest', VenvTask) {
152152
}
153153
tasks.register('testPythonWheel', VenvTask) {
154154
group = 'verification'
155-
description = 'Runs pytest against the installed Python wheel.'
155+
description = 'Runs pytest (with coverage) against the installed Python wheel.'
156156

157157
dependsOn installPythonWheelForSmokeTest
158158

159+
def coverageDir = layout.buildDirectory.dir('reports/coverage')
160+
159161
venvExec = 'python'
160-
args = ['-m', 'pytest', 'src/test/python']
162+
args = [
163+
'-m', 'pytest', 'src/test/python',
164+
'--cov=regi_python',
165+
'--cov-branch',
166+
'--cov-report=term-missing',
167+
"--cov-report=xml:${coverageDir.get().file('coverage.xml').asFile}",
168+
"--cov-report=html:${coverageDir.get().dir('html').asFile}",
169+
]
161170

162171
inputs.files(fileTree(dir: 'src/test/python', include: '**/*.py'))
163172
outputs.upToDateWhen { false }
173+
outputs.dir(coverageDir)
164174
}
165175

166176
check {

0 commit comments

Comments
 (0)