Skip to content

Commit c6e0cc5

Browse files
authored
chore: Run PR verification (unit tests, coverage, static analysis) on GitHub Actions (#3375)
1 parent ed2b86c commit c6e0cc5

4 files changed

Lines changed: 165 additions & 79 deletions

File tree

.github/workflows/codecov_code_coverage.yml

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Runs the unit-test gate on GitHub Actions, replacing the monolithic
2+
# CodeBuild `./gradlew build`. The unit-test suite runs exactly once (via Kover,
3+
# which also emits the coverage report), and static analysis runs in parallel so
4+
# cheap failures surface fast. Coverage upload is observability only and never
5+
# gates the merge.
6+
7+
name: PR Verification
8+
9+
on:
10+
pull_request:
11+
branches:
12+
- 'main'
13+
push:
14+
branches:
15+
- 'main'
16+
workflow_dispatch:
17+
18+
permissions:
19+
contents: read
20+
# Required for the test-report step to attach annotations to the run's check.
21+
# Note: fork PRs get a read-only token, so annotations are skipped there — the
22+
# step is continue-on-error so this never fails the required job, and the
23+
# actual test pass/fail is still determined by the Gradle exit code.
24+
checks: write
25+
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ github.ref }}
28+
# Only cancel superseded runs on PRs. Runs on `main` are never cancelled —
29+
# cancelling one would drop that commit's Codecov upload and leave a gap in
30+
# main's coverage history.
31+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
32+
33+
jobs:
34+
# REQUIRED. Runs the full unit-test suite once and uploads coverage to Codecov
35+
# as a soft-fail (observability-only) step.
36+
unit-tests:
37+
runs-on: ubuntu-latest
38+
env:
39+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
40+
41+
steps:
42+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
43+
44+
- name: Setup Java
45+
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
46+
with:
47+
java-version: '17'
48+
distribution: 'corretto'
49+
50+
- name: Setup Gradle
51+
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
52+
53+
# `koverXmlReport` runs the unit-test suite once and emits the coverage
54+
# report as a byproduct. Note: Kover only aggregates modules that apply the
55+
# Kover plugin, which is applied via PublishingConventionPlugin — so only
56+
# *published* modules have their tests run here. Every module with test
57+
# sources is currently also published, so this is at parity with the old
58+
# `./gradlew build` gate. A future non-published module with unit tests
59+
# would silently drop out of this gate (see design doc; follow-up: a CI
60+
# guard that fails if a module has test sources but no Kover plugin).
61+
- name: Run tests and generate Kover report
62+
run: ./gradlew koverXmlReport
63+
64+
# Surfaces failing tests as inline annotations on this job's check (no
65+
# separate check run — annotate_only). Runs even when the test step failed
66+
# (that's when it's most useful) and is soft-fail so a read-only token on
67+
# fork PRs never fails the job.
68+
- name: Publish test report annotations
69+
uses: mikepenz/action-junit-report@d9f48fc87bc235f7e214acf696ca5abc0a986f16 # v6.4.2
70+
if: ${{ !cancelled() }}
71+
continue-on-error: true
72+
with:
73+
report_paths: '**/build/test-results/**/*.xml'
74+
annotate_only: true
75+
76+
# Soft-fail: Codecov upload is observability only and must never fail this
77+
# required job. Skipped on fork PRs where the token is absent.
78+
- name: Upload coverage to Codecov
79+
if: ${{ !cancelled() && env.CODECOV_TOKEN != '' }}
80+
continue-on-error: true
81+
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
82+
with:
83+
name: report
84+
files: build/reports/kover/report.xml
85+
token: ${{ env.CODECOV_TOKEN }}
86+
87+
- name: Upload test reports
88+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
89+
if: failure()
90+
with:
91+
name: test-reports
92+
path: '**/reports/tests/**'
93+
retention-days: 7
94+
95+
# REQUIRED. Runs in parallel with unit-tests so lint/format/API-compat breaks
96+
# surface fast. `lint` is included because the old `./gradlew build` gate ran
97+
# Android lint with abortOnError/warningsAsErrors — dropping it would silently
98+
# remove a gating check.
99+
static-analysis:
100+
runs-on: ubuntu-latest
101+
102+
steps:
103+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
104+
105+
- name: Setup Java
106+
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
107+
with:
108+
java-version: '17'
109+
distribution: 'corretto'
110+
111+
- name: Setup Gradle
112+
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
113+
114+
- name: Run static analysis
115+
run: ./gradlew ktlintCheck checkstyle apiCheck lint
116+
117+
# REQUIRED. Verifies that every external type in each published module's public
118+
# API surface is resolvable on the compile classpath a downstream consumer
119+
# assembles from the published artifact. A failure means a dependency supplying
120+
# a public-API type is scoped `implementation` (runtime) but must be `api`
121+
# (compile). Runs in parallel with the other gates.
122+
scope-check:
123+
runs-on: ubuntu-latest
124+
# Only needs read access; the workflow-level `checks: write` (for the
125+
# test-report annotator) is not required here.
126+
permissions:
127+
contents: read
128+
129+
steps:
130+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
131+
132+
- name: Setup Java
133+
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
134+
with:
135+
java-version: '17'
136+
distribution: 'corretto'
137+
138+
- name: Setup Gradle
139+
uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a # v4.4.3
140+
141+
- name: Verify API scopes
142+
run: scripts/verify_api_scopes.sh

.github/workflows/verify_api_scopes.yml

Lines changed: 0 additions & 34 deletions
This file was deleted.

scripts/verify_api_scopes.sh

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ COORDS_FILE="$WORK/coords.txt"
4343
: > "$COORDS_FILE"
4444

4545
# Collect (projectPath -> coordinate) by running the task across all projects at once.
46-
(cd "$REPO" && ./gradlew -q printPublishedCoordinates 2>/dev/null) \
46+
# Must pass the SAME -PVERSION_NAME override used when publishing above: otherwise this reports
47+
# the real release version (e.g. 2.39.0) while the artifacts were published under $VER, so the
48+
# consumer would resolve released Central artifacts instead of the freshly-built ones — silently
49+
# validating the wrong bits for released modules and producing a false failure for any module not
50+
# yet released at that version. Modules that hard-override their version (apollo/appsync) still
51+
# report their own coordinate, since the override wins over the property.
52+
(cd "$REPO" && ./gradlew -q printPublishedCoordinates -PVERSION_NAME="$VER" 2>/dev/null) \
4753
| grep -E '^[a-z0-9.]+:[a-z0-9-]+:' | sort -u > "$COORDS_FILE" || true
4854

4955
if [ ! -s "$COORDS_FILE" ]; then
@@ -82,6 +88,22 @@ while IFS= read -r coord; do
8288
SKIP=$((SKIP+1)); continue
8389
fi
8490

91+
# Guard against the consumer's lenient artifactView silently swallowing an unresolved target
92+
# module: if the target isn't actually on the resolved classpath (e.g. a brand-new module not
93+
# yet published, or a skipped publish), the classpath comes back near-empty and the type check
94+
# would false-FAIL on EVERY public-API type. Require the target's own coordinate to appear on
95+
# the classpath before trusting the containment check. group and version are taken from $coord,
96+
# so version-overriding modules (apollo/appsync publish under their own group/version) are
97+
# handled. The artifact segment is left open ([^/]+) because KMP root coords resolve to their
98+
# `-android` sibling (foundation -> foundation-android). Both path layouts are matched: the
99+
# Gradle module cache uses a dotted group dir (com.amplifyframework/), mavenLocal uses a slashed
100+
# one (com/amplifyframework/).
101+
grp_slash="${group//./\/}"
102+
if ! grep -Eq "($group|$grp_slash)/[^/]+/$version/" "$CONSUMER/build/compile-classpath.txt"; then
103+
echo "SKIP [$coord]: target artifact did not resolve onto the consumer classpath (not published at $version?) — skipping to avoid a false scope failure"
104+
SKIP=$((SKIP+1)); continue
105+
fi
106+
85107
if python3 "$PYCHECK" --api-file "$api" \
86108
--classpath "$CONSUMER/build/compile-classpath.txt" --module "$coord"; then
87109
PASS=$((PASS+1))

0 commit comments

Comments
 (0)