diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties new file mode 100644 index 000000000..ba20c18e2 --- /dev/null +++ b/.github/ci-gradle.properties @@ -0,0 +1,9 @@ +# Copied to ~/.gradle/gradle.properties on CI runners, where it overrides the project file. +# Replaces --max-workers=1 from #1011: the OOM came from 4 workers sharing one 4 GB Kotlin daemon. +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 +kotlin.daemon.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g +org.gradle.workers.max=2 +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true +kotlin.incremental=false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27d22dfd7..472511c8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,17 +27,21 @@ env: # installs over the previous nightly without an uninstall. EXPECTED_NIGHTLY_SIGNER_SHA256: e42225b5ab3ac68236e54567ef0c4878afbafcdd3d438fbaba343e2d70425bf0 +# checks, tests, lint and build run on separate runners at the same time, and +# both publish jobs wait for all four. The CI Gradle properties replace the +# --max-workers=1 that #1011 needed when everything shared one runner. jobs: - build: - name: Build APK - runs-on: ubuntu-latest - timeout-minutes: 90 + checks: + name: Checks + runs-on: ubuntu-24.04 + timeout-minutes: 45 steps: - name: Checkout code uses: actions/checkout@v7 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + # Spotless ratchetFrom compares against an old commit. fetch-depth: 0 - name: Set up JDK 21 @@ -46,8 +50,16 @@ jobs: java-version: '21' distribution: 'temurin' + - name: Use CI Gradle properties + run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + # Read-only: every job writes its own Gradle User Home entry, and the + # repository cache is already at its 10 GB cap. This job restores from + # the build job's entry instead. - name: Set up Gradle uses: gradle/actions/setup-gradle@v6 + with: + cache-read-only: true - name: Grant execute permission for gradlew run: chmod +x gradlew @@ -63,6 +75,109 @@ jobs: ./gradlew spotlessCheck --build-cache --stacktrace + tests: + name: Unit tests + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + + - name: Use CI Gradle properties + run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Instrumentation tests are not executed in CI, but they must still + # compile: breakage here otherwise lands on main unnoticed. + - name: Run unit tests and compile instrumentation tests + run: > + ./gradlew :app:testGithubDebugUnitTest :app:testFossDebugUnitTest + :app:compileGithubDebugAndroidTestKotlin + --build-cache --stacktrace + + lint: + name: Lint + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + + - name: Use CI Gradle properties + run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + # Read-only for the same reason as the checks job. Lint is never the + # slowest job, so re-downloading its own dependencies costs no wall time. + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + with: + cache-read-only: true + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Lint GitHub nightly + if: ${{ !startsWith(github.ref, 'refs/tags/v') }} + run: > + ./gradlew :app:lintGithubNightly + --build-cache --stacktrace + + - name: Lint GitHub release + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: > + ./gradlew :app:lintGithubRelease + --build-cache --stacktrace + + # "Build APK" is the required status check in the Protect Main ruleset. + build: + name: Build APK + runs-on: ubuntu-24.04 + timeout-minutes: 90 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + + - name: Use CI Gradle properties + run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Decode Keystore id: keystore env: @@ -103,35 +218,13 @@ jobs: echo "Nightly keystore secret is not available. This run's nightly is debug-signed and is not published." fi - - name: Run unit tests - run: > - ./gradlew :app:testGithubDebugUnitTest :app:testFossDebugUnitTest - --max-workers=1 --build-cache --stacktrace - - # Instrumentation tests are not executed in CI, but they must still - # compile: breakage here otherwise lands on main unnoticed. - - name: Compile instrumentation tests - run: > - ./gradlew :app:compileGithubDebugAndroidTestKotlin - --build-cache --stacktrace - - - name: Lint GitHub nightly - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} - run: > - ./gradlew :app:lintGithubNightly - --build-cache --stacktrace - - - name: Lint GitHub release - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - run: > - ./gradlew :app:lintGithubRelease - --build-cache --stacktrace - + # GitHub release APKs are only published from tags, and nightly is + # initWith(release), so regular runs build nightly in their place. - name: Build CI APKs if: ${{ !startsWith(github.ref, 'refs/tags/v') }} run: > - ./gradlew :app:assembleGithubRelease :app:assembleGithubNightly :app:assembleFossRelease - --max-workers=1 --build-cache --stacktrace + ./gradlew :app:assembleGithubNightly :app:assembleFossRelease + --build-cache --stacktrace env: STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} @@ -146,7 +239,7 @@ jobs: if: ${{ startsWith(github.ref, 'refs/tags/v') }} run: > ./gradlew :app:assembleGithubRelease :app:assembleFossRelease - --max-workers=1 --build-cache --stacktrace + --build-cache --stacktrace env: STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} @@ -157,6 +250,8 @@ jobs: # Guards the IzzyOnDroid/F-Droid update path: a wrong or missing key here # is unrecoverable for installed users, so fail the build rather than # publish. Skipped when no keystore was available, e.g. on fork PRs. + # Regular runs have only the FOSS release APKs; the nightly is checked + # against its own key in the next step. - name: Verify release signing certificate if: ${{ steps.keystore.outputs.signed == 'true' }} run: | @@ -167,14 +262,22 @@ jobs: exit 1 fi - status=0 - for apk in \ - app/build/outputs/apk/github/release/app-github-universal-release.apk \ - app/build/outputs/apk/github/release/app-github-arm64-v8a-release.apk \ - app/build/outputs/apk/github/release/app-github-armeabi-v7a-release.apk \ - app/build/outputs/apk/foss/release/app-foss-universal-release.apk \ - app/build/outputs/apk/foss/release/app-foss-arm64-v8a-release.apk \ + apks=() + if [ "${GITHUB_REF#refs/tags/v}" != "$GITHUB_REF" ]; then + apks+=( + app/build/outputs/apk/github/release/app-github-universal-release.apk + app/build/outputs/apk/github/release/app-github-arm64-v8a-release.apk + app/build/outputs/apk/github/release/app-github-armeabi-v7a-release.apk + ) + fi + apks+=( + app/build/outputs/apk/foss/release/app-foss-universal-release.apk + app/build/outputs/apk/foss/release/app-foss-arm64-v8a-release.apk app/build/outputs/apk/foss/release/app-foss-armeabi-v7a-release.apk + ) + + status=0 + for apk in "${apks[@]}" do if [ ! -f "$apk" ]; then echo "::error::Missing expected APK $apk" @@ -218,15 +321,21 @@ jobs: shell: bash run: | shopt -s nullglob - apks=( - app/build/outputs/apk/github/release/app-github-universal-release*.apk - app/build/outputs/apk/github/release/app-github-arm64-v8a-release*.apk - app/build/outputs/apk/github/release/app-github-armeabi-v7a-release*.apk + apks=() + expected=3 + if [ "${GITHUB_REF#refs/tags/v}" != "$GITHUB_REF" ]; then + apks+=( + app/build/outputs/apk/github/release/app-github-universal-release*.apk + app/build/outputs/apk/github/release/app-github-arm64-v8a-release*.apk + app/build/outputs/apk/github/release/app-github-armeabi-v7a-release*.apk + ) + expected=6 + fi + apks+=( app/build/outputs/apk/foss/release/app-foss-universal-release*.apk app/build/outputs/apk/foss/release/app-foss-arm64-v8a-release*.apk app/build/outputs/apk/foss/release/app-foss-armeabi-v7a-release*.apk ) - expected=6 if (( ${#apks[@]} != expected )); then printf 'Expected %d published APKs, found %d.\n' "$expected" "${#apks[@]}" >&2 @@ -244,7 +353,7 @@ jobs: printf '| `%s` | %s | %s |\n' "$(basename "$apk")" "$size" "$bytes" done echo - echo '> GitHub and FOSS APKs are uploaded individually without compression. Nightly remains a compressed artifact on regular CI runs.' + echo '> Release APKs are uploaded individually without compression; GitHub release APKs are built on tags only. Nightly remains a compressed artifact on regular CI runs.' } >> "$GITHUB_STEP_SUMMARY" # IzzyOnDroid caps the FOSS APK at 30 MB; fail with a 0.5 MB margin so growth is seen before a release. @@ -257,6 +366,7 @@ jobs: # The universal APKs are what IzzyOnDroid and the README link to. - name: Upload GitHub universal APK + if: ${{ startsWith(github.ref, 'refs/tags/v') }} uses: actions/upload-artifact@v7 with: name: flow-github-universal-release-apk @@ -266,6 +376,7 @@ jobs: compression-level: 0 - name: Upload GitHub ARM64 APK + if: ${{ startsWith(github.ref, 'refs/tags/v') }} uses: actions/upload-artifact@v7 with: name: flow-github-arm64-release-apk @@ -275,6 +386,7 @@ jobs: compression-level: 0 - name: Upload GitHub ARMv7 APK + if: ${{ startsWith(github.ref, 'refs/tags/v') }} uses: actions/upload-artifact@v7 with: name: flow-github-armeabi-v7a-release-apk @@ -334,8 +446,8 @@ jobs: # builds read it by tag (UpdateRepository). nightly: name: Publish nightly - needs: build - runs-on: ubuntu-latest + needs: [checks, tests, lint, build] + runs-on: ubuntu-24.04 timeout-minutes: 15 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} permissions: @@ -377,8 +489,8 @@ jobs: release: name: Create Release - needs: build - runs-on: ubuntu-latest + needs: [checks, tests, lint, build] + runs-on: ubuntu-24.04 timeout-minutes: 20 if: startsWith(github.ref, 'refs/tags/v') permissions: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c47788968..9307532e0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,7 +35,7 @@ permissions: jobs: analyze: name: Analyze Kotlin/Java - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 60 permissions: security-events: write diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 99abae902..78eca9e0b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -222,6 +222,10 @@ android { } } +tasks.withType().configureEach { + maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) +} + // One profile for every shipped variant: foss, nightly and github run the same startup code, and // a per-variant profile left foss and nightly with library profiles only (#1301). baselineProfile {