1- name : Pull Request Build
1+ name : 26.x Pull Request Build
22# For pull requests: builds and test
3+ #
4+ # This name intentionally differs from "Pull Request Build". The default-branch
5+ # PR Test Report workflow listens for that name and compares results against the
6+ # master baseline, which is not maintained for 26.x.
37on :
48 push :
59 branches :
@@ -25,16 +29,12 @@ jobs:
2529 label : ' check'
2630 - gradle-argument : ' testWithJava11 testngWithJava11'
2731 label : ' java11'
28- test-results-dirs : ' testWithJava11 testngWithJava11'
2932 - gradle-argument : ' testWithJava17 testngWithJava17'
3033 label : ' java17'
31- test-results-dirs : ' testWithJava17 testngWithJava17'
3234 - gradle-argument : ' testWithJava21 testngWithJava21'
3335 label : ' java21'
34- test-results-dirs : ' testWithJava21 testngWithJava21'
35- - gradle-argument : ' test testng jacocoTestReport'
36+ - gradle-argument : ' test testng'
3637 label : ' java25'
37- test-results-dirs : ' test testng'
3838 - gradle-argument : ' jcstress'
3939 label : ' jcstress'
4040 steps :
@@ -46,177 +46,7 @@ jobs:
4646 java-version : ' 25'
4747 distribution : ' corretto'
4848 - name : build and test
49- run : |
50- if [ "${{ matrix.label }}" = "jcstress" ]; then
51- set -o pipefail
52- mkdir -p build
53- ./gradlew ${{matrix.gradle-argument}} --info --stacktrace 2>&1 | tee build/jcstress-output.txt
54- else
55- ./gradlew ${{matrix.gradle-argument}} --info --stacktrace
56- fi
57- - name : Upload Coverage HTML Report
58- uses : actions/upload-artifact@v7
59- if : always() && matrix.label == 'java25'
60- with :
61- name : jacoco-html-report
62- path : build/reports/jacoco/test/html/
63- retention-days : 14
64- - name : Upload Coverage XML Report
65- uses : actions/upload-artifact@v7
66- if : always() && matrix.label == 'java25'
67- with :
68- name : coverage-report
69- path : build/reports/jacoco/test/jacocoTestReport.xml
70- retention-days : 1
71- - name : Parse Test Results
72- if : always() && matrix.label != 'check' && matrix.label != 'jcstress'
73- run : |
74- total=0; failures=0; errors=0; skipped=0
75- for dir_name in ${{ matrix.test-results-dirs }}; do
76- dir="build/test-results/$dir_name"
77- for f in "$dir"/TEST-*.xml; do
78- [ -f "$f" ] || continue
79- t=$(grep -o 'tests="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
80- fl=$(grep -o 'failures="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
81- e=$(grep -o 'errors="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
82- s=$(grep -o 'skipped="[0-9]*"' "$f" | head -1 | grep -o '[0-9]*')
83- total=$((total + ${t:-0}))
84- failures=$((failures + ${fl:-0}))
85- errors=$((errors + ${e:-0}))
86- skipped=$((skipped + ${s:-0}))
87- done
88- done
89- passed=$((total - failures - errors - skipped))
90- mkdir -p /tmp/test-stats
91- echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failures,\"errors\":$errors,\"skipped\":$skipped}" \
92- > "/tmp/test-stats/${{ matrix.label }}.json"
93- - name : Parse jcstress Results
94- if : always() && matrix.label == 'jcstress'
95- run : |
96- total=0; passed=0; failed=0; errors=0; skipped=0
97- if [ -f build/jcstress-output.txt ]; then
98- line=$(grep 'Results:.*planned.*passed.*failed' build/jcstress-output.txt | tail -1)
99- if [ -n "$line" ]; then
100- total=$(echo "$line" | sed 's/.*Results: \([0-9]*\) planned.*/\1/')
101- passed=$(echo "$line" | sed 's/.*; \([0-9]*\) passed.*/\1/')
102- failed=$(echo "$line" | sed 's/.*passed, \([0-9]*\) failed.*/\1/')
103- soft=$(echo "$line" | sed 's/.*failed, \([0-9]*\) soft.*/\1/')
104- hard=$(echo "$line" | sed 's/.*soft errs, \([0-9]*\) hard.*/\1/')
105- errors=$((soft + hard))
106- fi
107- fi
108- mkdir -p /tmp/test-stats
109- echo "{\"total\":$total,\"passed\":$passed,\"failed\":$failed,\"errors\":$errors,\"skipped\":$skipped}" \
110- > "/tmp/test-stats/${{ matrix.label }}.json"
111- - name : Upload Test Stats
112- if : always() && matrix.label != 'check'
113- uses : actions/upload-artifact@v7
114- with :
115- name : test-stats-${{ matrix.label }}
116- path : /tmp/test-stats/${{ matrix.label }}.json
117- test-summary :
118- name : " Per-Class Coverage Gate"
119- needs : buildAndTest
120- if : >-
121- always() &&
122- github.event_name == 'pull_request' &&
123- github.event.pull_request.base.ref == 'master'
124- runs-on : ubuntu-latest
125- steps :
126- - uses : actions/checkout@v6
127- - name : Download Coverage Report
128- uses : actions/download-artifact@v8
129- continue-on-error : true
130- with :
131- name : coverage-report
132- path : coverage/
133- - name : Enforce Per-Class Coverage Gate
134- uses : actions/github-script@v8
135- with :
136- script : |
137- const fs = require('fs');
138- const path = require('path');
139- const { parseJacocoXml, pct, zeroCov, isRegression } = require('./.github/scripts/parse-jacoco.js');
140-
141- // --- Read baseline from repo ---
142- const baselineFile = 'test-baseline.json';
143- let baseline = { tests: {}, coverage: {} };
144- if (fs.existsSync(baselineFile)) {
145- baseline = JSON.parse(fs.readFileSync(baselineFile, 'utf8'));
146- }
147- const baseClasses = (baseline.coverage || {}).classes || {};
148-
149- // --- Parse JaCoCo XML for per-class coverage ---
150- const jacocoFile = path.join('coverage', 'jacocoTestReport.xml');
151- const parsed = parseJacocoXml(jacocoFile);
152- if (!parsed) {
153- console.log('No JaCoCo report found, skipping coverage gate.');
154- return;
155- }
156- const classCounters = parsed.classes;
157-
158- // --- Coverage gate: fail if any class regresses on any metric ---
159- // A regression requires BOTH a percentage drop AND an increase in the
160- // absolute number of missed items. This avoids false positives when
161- // well-covered code is extracted/moved out of a class (which lowers the
162- // percentage without actually losing any test coverage).
163- const regressions = [];
164- for (const [cls, curr] of Object.entries(classCounters)) {
165- const base = baseClasses[cls] || { line: zeroCov, branch: zeroCov, method: zeroCov };
166- const classRegressions = [];
167- for (const [metric, key] of [['Line', 'line'], ['Branch', 'branch'], ['Method', 'method']]) {
168- const currPct = pct(curr[key].covered, curr[key].missed);
169- const basePct = pct(base[key].covered, base[key].missed);
170- const currMissed = curr[key].missed;
171- const baseMissed = base[key].missed;
172- if (isRegression(currPct, basePct, currMissed, baseMissed)) {
173- classRegressions.push(` ${cls} ${metric}: ${currPct.toFixed(1)}% (was ${basePct.toFixed(1)}%, delta ${(currPct - basePct).toFixed(1)}%, missed: ${currMissed} was ${baseMissed})`);
174- }
175- }
176- if (classRegressions.length > 0) {
177- regressions.push(...classRegressions);
178- // Add method-level details for this regression
179- const currMethods = curr.methods || [];
180- const baseMethods = (base.methods || []);
181- if (currMethods.length > 0 || baseMethods.length > 0) {
182- const baseByKey = {};
183- for (const m of baseMethods) baseByKey[m.name + m.desc] = m;
184- const seen = new Set();
185- for (const m of currMethods) {
186- const key = m.name + m.desc;
187- seen.add(key);
188- const bm = baseByKey[key];
189- const currLinePct = pct(m.counters.line.covered, m.counters.line.missed);
190- const baseLinePct = bm ? pct(bm.counters.line.covered, bm.counters.line.missed) : null;
191- const lineChanged = baseLinePct !== null && Math.abs(currLinePct - baseLinePct) >= 0.05;
192- const isNew = !bm;
193- // When baseline method data exists, only show methods that actually
194- // changed or are new. Fall back to showing all uncovered methods
195- // when no baseline method data is available yet.
196- const hasMissed = m.counters.line.missed > 0 || m.counters.branch.missed > 0;
197- const show = baseMethods.length > 0
198- ? (lineChanged || isNew)
199- : (hasMissed || isNew);
200- if (show) {
201- const displayName = m.name === '<init>' ? 'constructor' : m.name;
202- let detail = ` ${displayName} — Line: ${currLinePct.toFixed(1)}%`;
203- if (baseLinePct !== null) detail += ` (was ${baseLinePct.toFixed(1)}%)`;
204- else detail += ' (new)';
205- regressions.push(detail);
206- }
207- }
208- for (const bm of baseMethods) {
209- if (!seen.has(bm.name + bm.desc)) {
210- const displayName = bm.name === '<init>' ? 'constructor' : bm.name;
211- regressions.push(` ${displayName} — removed`);
212- }
213- }
214- }
215- }
216- }
217- if (regressions.length > 0) {
218- core.setFailed(`Per-class coverage regressions detected:\n${regressions.join('\n')}\n\nUpdate test-baseline.json if these changes are intentional.`);
219- }
49+ run : ./gradlew ${{matrix.gradle-argument}} --info --stacktrace
22050 javadoc :
22151 runs-on : ubuntu-latest
22252 steps :
23363 if : always()
23464 needs :
23565 - buildAndTest
236- - test-summary
23766 - javadoc
23867 runs-on : ubuntu-latest
23968 steps :
@@ -243,10 +72,6 @@ jobs:
24372 echo "buildAndTest failed with result: ${{ needs.buildAndTest.result }}"
24473 exit 1
24574 fi
246- if [ "${{ needs.test-summary.result }}" != "success" ] && [ "${{ needs.test-summary.result }}" != "skipped" ]; then
247- echo "Per-Class Coverage Gate failed with result: ${{ needs.test-summary.result }}"
248- exit 1
249- fi
25075 if [ "${{ needs.javadoc.result }}" != "success" ]; then
25176 echo "javadoc failed with result: ${{ needs.javadoc.result }}"
25277 exit 1
0 commit comments