feat: upgrade build environment to JDK 21 and centralize CI configuration

- Create a composite GitHub Action `gradle-setup` to encapsulate code checkout, wrapper validation, JDK 21 setup, and Gradle caching logic.
- Update all GitHub workflows (`publish-core`, `codeql`, `scheduled-updates`, `release`, etc.) to utilize the new centralized `gradle-setup` action.
- Upgrade the project's primary JDK requirement from 17 to 21 across `jitpack.yml`, workflow files, and build-logic conventions.
- Refactor `KotlinAndroid.kt` and `build.gradle.kts` to target JVM 21 for the application while maintaining JVM 17 compatibility for published library modules (`api`, `model`, `proto`).
- Introduce a new `build-desktop` job in `reusable-check.yml` to verify desktop artifact assembly during CI.
- Implement dynamic `cache_read_only` detection in workflows to optimize Gradle cache usage across different branch types and merge groups.
- Update project documentation (`GEMINI.md`, `AGENTS.md`, `CONTRIBUTING.md`) to reflect the JDK 21 requirement and provide guidance on Robolectric configuration for the new version.
This commit is contained in:
James Rich
2026-03-27 09:25:33 -05:00
parent 9c9a1d7567
commit 8eb5970ca8
15 changed files with 130 additions and 167 deletions

38
.github/actions/gradle-setup/action.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Gradle Setup
description: Setup Java and Gradle for KMP builds
inputs:
cache_read_only:
description: 'Whether Gradle cache is read-only'
default: 'true'
jdk_distribution:
description: 'JDK distribution (temurin or jetbrains)'
default: 'temurin'
runs:
using: composite
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: 'recursive'
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: ${{ inputs.jdk_distribution }}
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ inputs.cache_read_only }}
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache-cleanup: on-success
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
add-job-summary: always

View File

@@ -8,7 +8,7 @@ For execution-focused recipes, see `docs/agent-playbooks/README.md`.
Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, decentralized mesh networks. The goal is to decouple business logic from the Android framework, enabling future expansion to iOS and other platforms while maintaining a high-performance native Android experience.
- **Language:** Kotlin (primary), AIDL.
- **Build System:** Gradle (Kotlin DSL). JDK 17 is REQUIRED.
- **Build System:** Gradle (Kotlin DSL). JDK 21 is REQUIRED.
- **Target SDK:** API 36. Min SDK: API 26 (Android 8.0).
- **Flavors:**
- `fdroid`: Open source only, no tracking/analytics.
@@ -99,7 +99,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
## 4. Execution Protocol
### A. Environment Setup
1. **JDK 17 MUST be used** to prevent Gradle sync/build failures.
1. **JDK 21 MUST be used** to prevent Gradle sync/build failures.
2. **Secrets:** You must copy `secrets.defaults.properties` to `local.properties`:
```properties
MAPS_API_KEY=dummy_key
@@ -128,7 +128,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
```
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
*Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
**CI workflow conventions (GitHub Actions):**
- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`.
@@ -154,6 +154,6 @@ Update documentation continuously as part of the same change. If you modify arch
## 5. Troubleshooting
- **Build Failures:** Check `gradle/libs.versions.toml` for dependency conflicts.
- **Missing Secrets:** Check `local.properties`.
- **JDK Version:** JDK 17 is required.
- **JDK Version:** JDK 21 is required.
- **Configuration Cache:** Add `--no-configuration-cache` flag if cache-related issues persist.
- **Koin Injection Failures:** Verify the KMP component is included in `app` root module wiring (`AppKoinModule`).

View File

@@ -70,7 +70,7 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
java-version: '21'
token: ${{ github.token }}
# Initializes the CodeQL tools for scanning.

View File

@@ -18,7 +18,7 @@ jobs:
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
java-version: 21
token: ${{ github.token }}
- name: Generate and submit dependency graph

View File

@@ -47,15 +47,8 @@ jobs:
submodules: 'recursive'
ref: ${{ inputs.ref || '' }}
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
- name: Build Dokka HTML documentation
run: ./gradlew dokkaGeneratePublicationHtml

View File

@@ -23,19 +23,8 @@ jobs:
with:
submodules: 'recursive'
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
- name: Configure Version
id: version

View File

@@ -119,25 +119,10 @@ jobs:
-Dorg.gradle.workers.max=4
-Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseParallelGC
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
ref: ${{ inputs.tag_name }}
fetch-depth: 0
submodules: 'recursive'
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
cache_read_only: 'false'
- name: Load secrets
env:
@@ -216,25 +201,10 @@ jobs:
-Dorg.gradle.workers.max=4
-Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseParallelGC
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
ref: ${{ inputs.tag_name }}
fetch-depth: 0
submodules: 'recursive'
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
cache_read_only: 'false'
- name: Load secrets
env:
@@ -288,27 +258,10 @@ jobs:
GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
ref: ${{ inputs.tag_name }}
fetch-depth: 0
submodules: 'recursive'
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
cache_read_only: 'false'
- name: Install dependencies for AppImage
if: runner.os == 'Linux'

View File

@@ -58,34 +58,24 @@ jobs:
permissions:
contents: read
timeout-minutes: 60
outputs:
cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: 'recursive'
- name: Determine cache read-only setting
id: cache_config
shell: bash
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.event_name }}" == "merge_group" ]] || [[ "${{ github.ref }}" == gh-readonly-queue/* ]]; then
echo "cache_read_only=false" >> "$GITHUB_OUTPUT"
else
echo "cache_read_only=true" >> "$GITHUB_OUTPUT"
fi
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.event_name != 'merge_group' && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/') }}
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache-cleanup: on-success
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
add-job-summary: always
cache_read_only: ${{ steps.cache_config.outputs.cache_read_only }}
- name: Code Style & Static Analysis
if: inputs.run_lint == true
@@ -138,38 +128,17 @@ jobs:
permissions:
contents: read
timeout-minutes: 60
needs: host-check
strategy:
fail-fast: true
matrix:
api_level: ${{ fromJson(inputs.api_levels) }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
fetch-depth: 0
submodules: 'recursive'
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.event_name != 'merge_group' && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/') }}
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache-cleanup: on-success
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
add-job-summary: always
cache_read_only: ${{ needs.host-check.outputs.cache_read_only }}
- name: Determine matrix metadata
id: matrix_meta
@@ -266,3 +235,28 @@ jobs:
**/build/outputs/androidTest-results
retention-days: 7
if-no-files-found: ignore
build-desktop:
name: Build Desktop
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 60
needs: host-check
steps:
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
cache_read_only: ${{ needs.host-check.outputs.cache_read_only }}
- name: Build Desktop
run: ./gradlew :desktop:assemble -Pci=true --scan
- name: Upload Desktop artifact
if: ${{ inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
name: desktop-app
path: desktop/build/libs/*.jar
retention-days: 7

View File

@@ -81,21 +81,10 @@ jobs:
- name: Fix file permissions
run: sudo chown -R $USER:$USER .
- name: Set up JDK 17
uses: actions/setup-java@v5
- name: Gradle Setup
uses: ./.github/actions/gradle-setup
with:
java-version: '17'
distribution: 'temurin'
token: ${{ github.token }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
build-scan-publish: true
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
build-scan-terms-of-use-agree: 'yes'
add-job-summary: always
cache_read_only: 'false'
- name: Update Graphs
run: ./gradlew graphUpdate

View File

@@ -8,7 +8,7 @@ For execution-focused recipes, see `docs/agent-playbooks/README.md`.
Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, decentralized mesh networks. The goal is to decouple business logic from the Android framework, enabling future expansion to iOS and other platforms while maintaining a high-performance native Android experience.
- **Language:** Kotlin (primary), AIDL.
- **Build System:** Gradle (Kotlin DSL). JDK 17 is REQUIRED.
- **Build System:** Gradle (Kotlin DSL). JDK 21 is REQUIRED.
- **Target SDK:** API 36. Min SDK: API 26 (Android 8.0).
- **Flavors:**
- `fdroid`: Open source only, no tracking/analytics.
@@ -100,7 +100,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
## 4. Execution Protocol
### A. Environment Setup
1. **JDK 17 MUST be used** to prevent Gradle sync/build failures.
1. **JDK 21 MUST be used** to prevent Gradle sync/build failures.
2. **Secrets:** You must copy `secrets.defaults.properties` to `local.properties`:
```properties
MAPS_API_KEY=dummy_key
@@ -129,7 +129,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
```
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
*Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
**CI workflow conventions (GitHub Actions):**
- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`.
@@ -155,6 +155,6 @@ Update documentation continuously as part of the same change. If you modify arch
## 5. Troubleshooting
- **Build Failures:** Check `gradle/libs.versions.toml` for dependency conflicts.
- **Missing Secrets:** Check `local.properties`.
- **JDK Version:** JDK 17 is required.
- **JDK Version:** JDK 21 is required.
- **Configuration Cache:** Add `--no-configuration-cache` flag if cache-related issues persist.
- **Koin Injection Failures:** Verify the KMP component is included in `app` root module wiring (`AppKoinModule`).

View File

@@ -48,7 +48,7 @@ Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI t
- **Unit tests** are located in the `src/test/` directory of each module.
- **Compose UI Tests (JVM)** are preferred for component testing and are also located in `src/test/` using **Robolectric**.
- Note: If using Java 17, pin your Robolectric tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility issues.
- Note: If using Java 21, pin your Robolectric tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility issues.
- **Instrumented tests** (including full E2E UI tests) are located in `src/androidTest/`. For Compose UI, use the [Jetpack Compose Testing APIs](https://developer.android.com/jetpack/compose/testing).
#### Guidelines for Testing

View File

@@ -8,7 +8,7 @@ For execution-focused recipes, see `docs/agent-playbooks/README.md`.
Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, decentralized mesh networks. The goal is to decouple business logic from the Android framework, enabling future expansion to iOS and other platforms while maintaining a high-performance native Android experience.
- **Language:** Kotlin (primary), AIDL.
- **Build System:** Gradle (Kotlin DSL). JDK 17 is REQUIRED.
- **Build System:** Gradle (Kotlin DSL). JDK 21 is REQUIRED.
- **Target SDK:** API 36. Min SDK: API 26 (Android 8.0).
- **Flavors:**
- `fdroid`: Open source only, no tracking/analytics.
@@ -96,7 +96,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
## 4. Execution Protocol
### A. Environment Setup
1. **JDK 17 MUST be used** to prevent Gradle sync/build failures.
1. **JDK 21 MUST be used** to prevent Gradle sync/build failures.
2. **Secrets:** You must copy `secrets.defaults.properties` to `local.properties`:
```properties
MAPS_API_KEY=dummy_key
@@ -125,7 +125,7 @@ Always run commands in the following order to ensure reliability. Do not attempt
./gradlew testFdroidDebug testGoogleDebug # Flavor-specific unit tests
./gradlew lintFdroidDebug lintGoogleDebug # Flavor-specific lint checks
```
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
*Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
**CI workflow conventions (GitHub Actions):**
- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`.
@@ -151,6 +151,6 @@ Update documentation continuously as part of the same change. If you modify arch
## 5. Troubleshooting
- **Build Failures:** Check `gradle/libs.versions.toml` for dependency conflicts.
- **Missing Secrets:** Check `local.properties`.
- **JDK Version:** JDK 17 is required.
- **JDK Version:** JDK 21 is required.
- **Configuration Cache:** Add `--no-configuration-cache` flag if cache-related issues persist.
- **Koin Injection Failures:** Verify the KMP component is included in `app` root module wiring (`AppKoinModule`).

View File

@@ -25,14 +25,14 @@ plugins {
group = "org.meshtastic.buildlogic"
// Configure the build-logic plugins to target JDK 17
// Configure the build-logic plugins to target JDK 21
// This improves compatibility for developers building the project or consuming its libraries.
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } }
kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_21 } }
dependencies {
// This allows the use of the 'libs' type-safe accessor in the Kotlin source of the plugins

View File

@@ -49,8 +49,13 @@ internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) {
defaultConfig.targetSdk = targetSdkVersion
}
compileOptions.sourceCompatibility = JavaVersion.VERSION_17
compileOptions.targetCompatibility = JavaVersion.VERSION_17
val javaVersion = if (project.name in listOf("api", "model", "proto")) {
JavaVersion.VERSION_17
} else {
JavaVersion.VERSION_21
}
compileOptions.sourceCompatibility = javaVersion
compileOptions.targetCompatibility = javaVersion
}
configureMokkery()
@@ -170,9 +175,10 @@ internal fun Project.configureKotlinJvm() {
/** Configure base Kotlin options */
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
extensions.configure<T> {
// Using Java 17 for better compatibility with consumers (e.g. plugins, older environments)
// while still supporting modern Kotlin features.
jvmToolchain(17)
val javaVersion = if (project.name in listOf("api", "model", "proto")) 17 else 21
// Using Java 17 for published modules for better compatibility with consumers (e.g. plugins, older environments),
// and Java 21 for the rest of the app.
jvmToolchain(javaVersion)
if (this is KotlinMultiplatformExtension) {
targets.configureEach {
@@ -201,7 +207,8 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
val isPublishedModule = project.name in listOf("api", "model", "proto")
jvmTarget.set(if (isPublishedModule) JvmTarget.JVM_17 else JvmTarget.JVM_21)
allWarningsAsErrors.set(warningsAsErrors)
freeCompilerArgs.addAll(
// Enable experimental coroutines APIs, including Flow

View File

@@ -1,5 +1,5 @@
jdk:
- openjdk17
- openjdk21
before_install:
- ./gradlew --stop
install: