name: Test/Build on: pull_request: branches: [main] push: branches: [main] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v6 - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 # Remote Gradle build cache: writes on push to main, reads on PRs and # other branches. Caches both `~/.gradle/caches/` and individual task # outputs, so dependency-only changes hit the cache and skip recompiling # downstream modules / re-merging native libs (~600MB of work on # :amethyst alone). Replaces the narrower `cache: gradle` previously on # actions/setup-java, which only cached `modules-2`. - name: Set up Gradle uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} - name: Linter (gradle) run: ./gradlew spotlessCheck :quartz:verifyKmpPurity :commons:verifyKmpPurity build-desktop: needs: lint strategy: fail-fast: false matrix: include: - os: ubuntu-latest desktop-task: packageDeb desktop-artifact-name: Desktop Linux DEB desktop-artifact-path: desktopApp/build/compose/binaries/main/deb/*.deb - os: macos-latest desktop-task: packageDmg desktop-artifact-name: Desktop macOS DMG desktop-artifact-path: desktopApp/build/compose/binaries/main/dmg/*.dmg - os: windows-latest desktop-task: packageMsi desktop-artifact-name: Desktop Windows MSI desktop-artifact-path: desktopApp/build/compose/binaries/main/msi/*.msi runs-on: ${{ matrix.os }} timeout-minutes: 60 defaults: run: shell: bash steps: - name: Checkout code uses: actions/checkout@v6 - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Set up Gradle uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} # Compose UI smoke test (DesktopLaunchSmokeTest) uses Skiko which needs # a display server on Linux. xvfb provides a virtual framebuffer. - name: Install xvfb (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y xvfb - name: Test + Build Desktop (gradle) run: | CMD="./gradlew :quartz:jvmTest :commons:jvmTest :nestsClient:jvmTest :cli:test :desktopApp:test :desktopApp:${{ matrix.desktop-task }}" if [ "${{ runner.os }}" = "Linux" ]; then xvfb-run --auto-servernum $CMD else $CMD fi # jpackage pins libicu to the build host's version (libicu74 on # ubuntu-24.04). Rewrite the .deb so testers on other Debian/Ubuntu # releases can install the uploaded artifact. - name: Relax libicu dependency in .deb if: matrix.desktop-task == 'packageDeb' run: | set -euo pipefail chmod +x scripts/relax-deb-libicu.sh scripts/relax-deb-libicu.sh desktopApp/build/compose/binaries/main/deb/*.deb - name: Upload Desktop Distribution uses: actions/upload-artifact@v7 with: name: ${{ matrix.desktop-artifact-name }} path: ${{ matrix.desktop-artifact-path }} test-quartz-ios: # Phase 1 of the iOS support plan # (amethyst/plans/2026-05-24-ios-support.md): keep :quartz green on iOS # so JVM-only imports can't sneak into commonMain unnoticed. The # `verifyKmpPurity` task in the lint job is the fast pre-check (Linux, # ~1s); this job is the real one — compiles for the device variant # and actually runs the simulator test suite. needs: lint runs-on: macos-latest timeout-minutes: 45 steps: - name: Checkout code uses: actions/checkout@v6 - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Set up Gradle uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} # Two tasks, two purposes: # - iosSimulatorArm64Test runs the existing iosTest suite on the # simulator (NIP-04 / NIP-17 / NIP-19 / NIP-49 / AES-GCM / # Chatroom keys), exercising secp256k1 and CryptoKit-backed # primitives on a real Apple toolchain. # - compileTestKotlinIosArm64 catches any device-only compile drift # (iosArm64 = aarch64-apple-ios) without needing a physical # device to run on. Compile-only is enough — running on-device # would require xcodebuild + a provisioning profile. - name: Test Quartz on iOS run: | ./gradlew \ :quartz:iosSimulatorArm64Test \ :quartz:compileTestKotlinIosArm64 # :commons gained iosArm64 + iosSimulatorArm64 targets in Phase 2 of the # iOS plan. Actual UI / lifecycle wiring will land with the iosApp module # in Phase 3, but the shared commonMain + commonTest sources are already # built here against an Apple Native frontend. Same two-task shape as # quartz above: # - iosSimulatorArm64Test compiles AND runs the shared commonTest suite # on the simulator. commonTest is built for every target, so a test # reaching for a JVM-only API (JUnit, javaClass, @JvmStatic, or a # jvmAndroid-only symbol) breaks the Apple build even though # :commons:jvmTest stays green — this is the job that catches it. # - compileTestKotlinIosArm64 catches device-only compile drift # (iosArm64 = aarch64-apple-ios) without needing a physical device. - name: Test Commons on iOS run: | ./gradlew \ :commons:iosSimulatorArm64Test \ :commons:compileTestKotlinIosArm64 - name: Upload iOS Test Reports uses: actions/upload-artifact@v7 if: failure() with: name: Quartz iOS Test Reports path: quartz/build/reports test-and-build-android: needs: lint runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Checkout code uses: actions/checkout@v6 - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Set up Gradle uses: gradle/actions/setup-gradle@v6 with: cache-read-only: ${{ github.ref != 'refs/heads/main' }} # Lint + focused unit tests + benchmark assembly in one Gradle invocation. # Previously: one invocation for lint, one for `test` (which compiled all # six amethyst variants × all flavors), one for `assembleBenchmark` # (re-walking the same task graph). Combining them keeps the daemon hot # across phases and lets task-level dedup (e.g. compileKotlin) only # happen once. # # `-PdisableAbiSplits=true` produces a single non-split APK per # (flavor, buildType) instead of 5 (4 ABIs + universal). The CI only # uploads the universal-equivalent benchmark APK; per-ABI splits were # being built and discarded, costing ~600MB of stripped_native_libs # intermediates and several minutes per run. # # Test scope: only Debug unit tests for amethyst. The release/benchmark # variants are compile-equivalent for unit-test purposes; running all six # adds ~5× the kotlinc work without catching new defects on PRs. Push to # main still gets the full test matrix via the production-build path. - name: Test + Build Android (gradle) run: | ./gradlew \ :amethyst:lintFdroidBenchmark \ :amethyst:lintPlayBenchmark \ :quartz:jvmTest \ :commons:jvmTest \ :nestsClient:jvmTest \ :amethyst:testFdroidDebugUnitTest \ :amethyst:testPlayDebugUnitTest \ :amethyst:assembleBenchmark \ -PdisableAbiSplits=true - name: Upload Android Lint Reports uses: actions/upload-artifact@v7 if: always() with: name: Android Lint Reports path: amethyst/build/reports/lint-results-*.html - name: Android Test Report uses: asadmansr/android-test-report-action@v1.2.0 if: always() - name: Upload Test Results uses: actions/upload-artifact@v7 if: failure() with: name: Test Reports path: amethyst/build/reports # With -PdisableAbiSplits=true the APK is named without the ABI/universal # suffix: amethyst--benchmark.apk. Glob both forms so this still # works if a contributor runs CI on a branch that doesn't pass the flag. - name: Upload Play APK Benchmark uses: actions/upload-artifact@v7 with: name: Play Benchmark APK path: | amethyst/build/outputs/apk/play/benchmark/amethyst-play-benchmark.apk amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk - name: Upload FDroid APK Benchmark uses: actions/upload-artifact@v7 with: name: FDroid Benchmark APK path: | amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-benchmark.apk amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk - name: Upload Compose Reports uses: actions/upload-artifact@v7 with: name: Compose Reports path: amethyst/build/compose_compiler