diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 70d0696e1f..091cf431ca 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -111,11 +111,13 @@ jobs: - name: Setup Node and install dependencies uses: ./.github/actions/setup-node-pnpm - - name: Setup Java 17 + # FNXC:MobileAndroidBuild 2026-06-28-00:00: + # Capacitor 7 @capacitor/android compiles its Android library with JavaVersion.VERSION_21; manual Android Gradle builds must provision JDK 21 because JDK 17 fails with `invalid source release: 21`. + - name: Setup Java 21 uses: actions/setup-java@v4 with: distribution: temurin - java-version: "17" + java-version: "21" - name: Cache Android Gradle caches uses: actions/cache@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c1d69f153..a3343e945c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -389,11 +389,13 @@ jobs: - name: Setup Node and install dependencies uses: ./.github/actions/setup-node-pnpm - - name: Setup Java 17 + # FNXC:MobileAndroidBuild 2026-06-28-00:00: + # Capacitor 7 @capacitor/android compiles its Android library with JavaVersion.VERSION_21; release APK/AAB Gradle builds must provision JDK 21 because JDK 17 fails with `invalid source release: 21`. + - name: Setup Java 21 uses: actions/setup-java@v4 with: distribution: temurin - java-version: "17" + java-version: "21" - name: Cache Android Gradle caches uses: actions/cache@v4 diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index a314be59a1..7edd2ca8fc 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -375,11 +375,13 @@ jobs: - name: Setup Node and install dependencies uses: ./.github/actions/setup-node-pnpm - - name: Setup Java 17 + # FNXC:MobileAndroidBuild 2026-06-28-00:00: + # Capacitor 7 @capacitor/android compiles its Android library with JavaVersion.VERSION_21; rehearsal APK/AAB Gradle builds must provision JDK 21 because JDK 17 fails with `invalid source release: 21`. + - name: Setup Java 21 uses: actions/setup-java@v4 with: distribution: temurin - java-version: "17" + java-version: "21" - name: Cache Android Gradle caches uses: actions/cache@v4 diff --git a/MOBILE.md b/MOBILE.md index 11ea34a507..ccc13e3f3d 100644 --- a/MOBILE.md +++ b/MOBILE.md @@ -8,7 +8,7 @@ Fusion mobile builds package the dashboard web client into Capacitor shells via - **pnpm** 10+ - **Xcode** (iOS builds) - **Android Studio** (Android SDK + emulator tooling) -- **Java JDK** 17+ (Android Gradle builds) +- **Java JDK** 21+ (Android Gradle builds) ## Quick Start @@ -160,7 +160,7 @@ You can also use ImageMagick if preferred. ### Android build fails -- Verify Java 17+ (`java -version`) +- Verify Java 21+ (`java -version`) - Confirm Android SDK and Gradle tooling are installed via Android Studio ### PWA does not install diff --git a/packages/cli/src/__tests__/ci-workflow.test.ts b/packages/cli/src/__tests__/ci-workflow.test.ts index 1ce84698f4..58e3f91d00 100644 --- a/packages/cli/src/__tests__/ci-workflow.test.ts +++ b/packages/cli/src/__tests__/ci-workflow.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll } from "vitest"; -import { readFileSync, accessSync, constants } from "node:fs"; +import { readFileSync, accessSync, constants, existsSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { parse } from "yaml"; @@ -27,6 +27,84 @@ function findCompositeSetupStep(steps: any[]) { return steps.find((step) => step.uses === "./.github/actions/setup-node-pnpm"); } +function findSetupJavaStep(steps: any[]) { + return steps.find((step) => typeof step.uses === "string" && step.uses.startsWith("actions/setup-java@")); +} + +function resolveCapacitorAndroidGradlePath(): string { + const primaryPath = join( + workspaceRoot, + "packages", + "mobile", + "node_modules", + "@capacitor", + "android", + "capacitor", + "build.gradle", + ); + if (existsSync(primaryPath)) { + return primaryPath; + } + + const pnpmStoreCandidates = [ + join(workspaceRoot, "node_modules", ".pnpm"), + join(workspaceRoot, "packages", "mobile", "node_modules", ".pnpm"), + ]; + + for (const pnpmStore of pnpmStoreCandidates) { + if (!existsSync(pnpmStore)) { + continue; + } + + for (const entry of readdirSync(pnpmStore)) { + if (!entry.startsWith("@capacitor+android@")) { + continue; + } + + const candidate = join( + pnpmStore, + entry, + "node_modules", + "@capacitor", + "android", + "capacitor", + "build.gradle", + ); + if (existsSync(candidate)) { + return candidate; + } + } + } + + throw new Error( + "Unable to resolve @capacitor/android capacitor/build.gradle from packages/mobile/node_modules or the pnpm store", + ); +} + +function readCapacitorAndroidSourceCompatibilityMajor(): number { + const gradleContent = readFileSync(resolveCapacitorAndroidGradlePath(), "utf-8"); + const match = gradleContent.match(/sourceCompatibility\s+JavaVersion\.VERSION_(\d+)/); + if (!match) { + throw new Error("Unable to parse @capacitor/android sourceCompatibility JavaVersion.VERSION_"); + } + return Number.parseInt(match[1], 10); +} + +function expectAndroidBuildJobJavaVersionAtLeast(workflow: any, workflowName: string, minimumJavaVersion: number) { + const buildAndroidJob = workflow.jobs?.["build-android"]; + expect(buildAndroidJob, `${workflowName} must define a build-android job`).toBeDefined(); + + const setupJavaStep = findSetupJavaStep(buildAndroidJob?.steps ?? []); + expect(setupJavaStep, `${workflowName} build-android must provision Java`).toBeDefined(); + expect(setupJavaStep.name).toBe("Setup Java 21"); + + const javaVersion = setupJavaStep.with?.["java-version"]; + expect(javaVersion, `${workflowName} build-android must pin JDK 21`).toBe("21"); + expect(Number.parseInt(javaVersion, 10), `${workflowName} JDK must satisfy @capacitor/android sourceCompatibility`).toBeGreaterThanOrEqual( + minimumJavaVersion, + ); +} + describe("Merge gate (.github/workflows/pr-checks.yml)", () => { let workflow: any; let content: string; @@ -342,6 +420,25 @@ describe("Version & Release workflow (.github/workflows/version.yml)", () => { }); }); +describe("Android build JDK compatibility", () => { + let capacitorAndroidSourceCompatibility: number; + + beforeAll(() => { + capacitorAndroidSourceCompatibility = readCapacitorAndroidSourceCompatibilityMajor(); + }); + + /* + FNXC:MobileAndroidBuild 2026-06-28-00:00: + Android CI jobs that run Capacitor Gradle builds must provision a JDK version greater than or equal to @capacitor/android's sourceCompatibility. FN-7209 proved JDK 17 silently drifted below Capacitor 7's JavaVersion.VERSION_21 requirement and failed release builds with `invalid source release: 21`. + */ + it("pins every Android-building workflow to a JDK compatible with Capacitor", () => { + for (const workflowName of ["release.yml", "test-release.yml", "mobile.yml"]) { + const workflow = loadWorkflow(workflowName).parsed; + expectAndroidBuildJobJavaVersionAtLeast(workflow, workflowName, capacitorAndroidSourceCompatibility); + } + }); +}); + describe("Binary release workflow (.github/workflows/release.yml)", () => { let workflow: any; let content: string;