Skip to content

PR Checks

PR Checks #267

Workflow file for this run

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