Repository navigation
PR Checks #267
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Checks | |
| env: | |
| GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:MaxMetaspaceSize=1g" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false | |
| on: | |
| pull_request: | |
| types: [opened, reopened, synchronize] | |
| branches: | |
| - master | |
| # Always start. Required check names only exist after a run is created. | |
| # Docs/assets-only PRs still report those names; Gradle/Xcode stay inside the jobs. | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| classify: | |
| name: Classify PR paths | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| product: ${{ steps.classify.outputs.product }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Classify product vs docs-only | |
| id: classify | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT_NAME" != "pull_request" ]; then | |
| echo "product=true" >> "$GITHUB_OUTPUT" | |
| echo "Non-PR event: run full product checks" | |
| exit 0 | |
| fi | |
| if ! git cat-file -e "${BASE_SHA}^{commit}"; then | |
| git fetch --no-tags --prune --depth=1 origin "$BASE_SHA" | |
| fi | |
| product=false | |
| while IFS= read -r f; do | |
| [ -z "$f" ] && continue | |
| if [[ "$f" == *.md || "$f" == fastlane/* || "$f" == static/* || "$f" == captures/* ]]; then | |
| continue | |
| fi | |
| # Official / project skill mirrors: not product code. Wear samples | |
| # ship as *.kt.md.txt, which would otherwise trip this gate. | |
| if [[ "$f" == skills/* || "$f" == .agents/skills/* || "$f" == .claude/skills/* ]]; then | |
| continue | |
| fi | |
| product=true | |
| break | |
| done < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA") | |
| echo "product=$product" >> "$GITHUB_OUTPUT" | |
| echo "product=$product" | |
| build: | |
| name: Compile + migration safety net (assembleDebug, :shared:desktopTest, app unit tests) | |
| needs: classify | |
| if: ${{ always() && !cancelled() }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| env: | |
| RUN_PRODUCT: ${{ needs.classify.result != 'success' || needs.classify.outputs.product == 'true' }} | |
| # S4d-169 (merge of origin/master): adopt master's scaffolding (workflow_dispatch, | |
| # permissions, concurrency, wrapper validation, setup-android) BUT keep the migration safety net | |
| # (full tests, not master's compile-only `-x test`). The master `if: !github.event.pull_request.draft` | |
| # guard is deliberately DROPPED so Draft PR #358 still runs :shared:desktopTest + the non-strict | |
| # watermark golden net (device-independent nonblank/geometry/decal/encode structure). | |
| # S4d-172: the pinned-environment `WATERMARK_GOLDEN_STRICT=true` FNV gate was REMOVED from CI - its | |
| # baselines are Robolectric-environment pixel hashes captured on a pinned host (see | |
| # WatermarkExportGoldenTest's policy comment + ADR-0010) and cannot pass on GitHub Ubuntu/Temurin. The | |
| # strict gate stays a LOCAL / pinned-env gate, run on the capture env or an intentional | |
| # Robolectric/Skia/font bump via `WATERMARK_GOLDEN_STRICT=true ./gradlew :app:testDebugUnitTest`. | |
| steps: | |
| - name: Docs-only — report required compile check | |
| if: env.RUN_PRODUCT != 'true' | |
| run: echo "Docs/assets-only PR; compile gate reported without Gradle." | |
| - name: Checkout | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Validate Gradle Wrapper | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: gradle/wrapper-validation-action@v2 | |
| - name: Set up JDK 17 | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Set up Android SDK | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| # Default packages are `tools platform-tools`. Google stopped serving | |
| # the legacy `tools` package on 2026-09-15, so sdkmanager fails with | |
| # "Failed to find package 'tools'". cmdline-tools already come from | |
| # this action; only platform-tools is still needed. | |
| # https://github.com/android-actions/setup-android/issues/537 | |
| packages: platform-tools | |
| - name: Set up Gradle | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: gradle/actions/setup-gradle@v4 | |
| with: | |
| gradle-version: wrapper | |
| cache-read-only: ${{ github.event_name == 'pull_request' }} | |
| gradle-home-cache-cleanup: true | |
| - name: Make gradlew executable | |
| if: env.RUN_PRODUCT == 'true' | |
| run: chmod +x ./gradlew | |
| # J2 — static privacy/backup policy must stay explicit on every PR (issue 13 §J2). | |
| - name: Android backup / data-extraction policy structural check | |
| if: env.RUN_PRODUCT == 'true' | |
| run: | | |
| set -euo pipefail | |
| grep -q 'android:dataExtractionRules="@xml/data_extraction_rules"' app/src/main/AndroidManifest.xml | |
| grep -q 'android:fullBackupContent="@xml/backup_rules"' app/src/main/AndroidManifest.xml | |
| test -f app/src/main/res/xml/data_extraction_rules.xml | |
| test -f app/src/main/res/xml/backup_rules.xml | |
| echo "J2 backup policy structural check OK" | |
| - name: Build apk with Gradle | |
| if: env.RUN_PRODUCT == 'true' | |
| run: ./gradlew :app:assembleDebug | |
| # CI-P1: downloadable debug APK for review (debug-signed, not Play/release-ready). | |
| - name: Upload unsigned debug APK artifact | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: easywatermark-android-debug-${{ github.sha }}-UNSIGNED-DEBUG | |
| path: app/build/outputs/apk/debug/*.apk | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - name: Run :shared multiplatform tests (JVM, CMP plan C1.8) | |
| if: env.RUN_PRODUCT == 'true' | |
| run: ./gradlew :shared:desktopTest | |
| # ADR-0031: Desktop packaging left the PR gate; this first-party compile step is | |
| # the remaining PR-time desktop net (peers gate PRs on compile/test, not installers). | |
| - name: Compile :desktopApp (PR-time desktop net, no packaging) | |
| if: env.RUN_PRODUCT == 'true' | |
| run: ./gradlew :desktopApp:classes | |
| - name: Run :app unit tests incl. watermark golden (Robolectric NATIVE, CMP plan C1.7/C1.8) | |
| if: env.RUN_PRODUCT == 'true' | |
| run: ./gradlew :app:testDebugUnitTest | |
| # J2: lint is informative — do not fail the PR on pre-existing lint debt (fail-open). | |
| - name: Lint Debug (J2 residual-tolerant) | |
| if: env.RUN_PRODUCT == 'true' | |
| continue-on-error: true | |
| run: ./gradlew :app:lintDebug --max-workers=4 | |
| # J1 — permanent iOS PR gate (issue 13 §J1 / plan 52). | |
| # Ubuntu cannot build Kotlin/Native iOS or the Swift host; this job is the non-local-only compile | |
| # net so iOS breakage cannot merge on Linux-only green. XCUITest stays residual (nightly/release). | |
| ios: | |
| name: iOS simulator tests + host build (macos) | |
| needs: classify | |
| if: ${{ always() && !cancelled() }} | |
| runs-on: ${{ (needs.classify.result == 'success' && needs.classify.outputs.product != 'true') && 'ubuntu-latest' || 'macos-latest' }} | |
| timeout-minutes: 60 | |
| env: | |
| RUN_PRODUCT: ${{ needs.classify.result != 'success' || needs.classify.outputs.product == 'true' }} | |
| steps: | |
| - name: Docs-only — report required iOS check | |
| if: env.RUN_PRODUCT != 'true' | |
| run: echo "Docs/assets-only PR; iOS gate reported without Xcode." | |
| - name: Checkout | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Validate Gradle Wrapper | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: gradle/wrapper-validation-action@v2 | |
| - name: Set up JDK 17 | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Set up Gradle | |
| if: env.RUN_PRODUCT == 'true' | |
| uses: gradle/actions/setup-gradle@v4 | |
| with: | |
| gradle-version: wrapper | |
| cache-read-only: ${{ github.event_name == 'pull_request' }} | |
| gradle-home-cache-cleanup: true | |
| - name: Make gradlew executable | |
| if: env.RUN_PRODUCT == 'true' | |
| run: chmod +x ./gradlew | |
| - name: :shared:iosSimulatorArm64Test (K/N compile + tests) | |
| if: env.RUN_PRODUCT == 'true' | |
| run: ./gradlew :shared:iosSimulatorArm64Test --max-workers=4 | |
| # Framework link: iosApp Xcode run-script phase runs | |
| # :shared:embedAndSignAppleFrameworkForXcode (needs JAVA_HOME from setup-java). | |
| - name: iosApp generic iOS Simulator build (unsigned) | |
| if: env.RUN_PRODUCT == 'true' | |
| run: | | |
| xcodebuild \ | |
| -project iosApp/iosApp.xcodeproj \ | |
| -scheme iosApp \ | |
| -configuration Debug \ | |
| -sdk iphonesimulator \ | |
| -destination 'generic/platform=iOS Simulator' \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| build |