diff --git a/.github/workflows/android-dev-client.yml b/.github/workflows/android-dev-client.yml new file mode 100644 index 00000000..d3c5556c --- /dev/null +++ b/.github/workflows/android-dev-client.yml @@ -0,0 +1,94 @@ +name: Android dev client + +# Builds the debug Expo dev client of apps/community-mobile for arm64 Android. The agent box (CLAUDE.md +# ยง11) installs it on its containerized Android device and runs the branch under test's JavaScript in +# it through Metro, so the native app is built here (Google's Android build tools do not run on that +# arm64 Linux box) and only when its native side changes. +# +# The artifact is named after the app's native fingerprint (scripts/native-fingerprint.mjs): one build +# serves every branch whose native side matches, and a run whose fingerprint already has a live +# artifact stops before building. Debug-signed, no secrets, nothing leaves this repo's artifacts. +# +# push to main / pull request touching the app or its dependencies -> build when the fingerprint is new +# manual run -> same, for any branch + +on: + push: + branches: [main] + paths: &app_paths + - "apps/community-mobile/**" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - ".npmrc" + - "patches/**" + - ".github/workflows/android-dev-client.yml" + pull_request: + branches: [main] + paths: *app_paths + workflow_dispatch: + +permissions: + contents: read + actions: read + +concurrency: + group: android-dev-client-${{ github.ref }} + cancel-in-progress: true + +jobs: + dev-client: + name: Android dev client (arm64) + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + CI: "true" + EXPO_NO_DOTENV: "1" + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.12.0 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + + - name: Fingerprint the native side + id: fingerprint + working-directory: apps/community-mobile + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + hash="$(node scripts/native-fingerprint.mjs)" + name="android-dev-client-$hash" + live="$(gh api "repos/$GITHUB_REPOSITORY/actions/artifacts?name=$name" --jq '[.artifacts[] | select(.expired | not)] | length')" + echo "name=$name" >> "$GITHUB_OUTPUT" + echo "live=$live" >> "$GITHUB_OUTPUT" + echo "fingerprint $hash, live artifacts: $live" + + - if: steps.fingerprint.outputs.live == '0' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - if: steps.fingerprint.outputs.live == '0' + uses: gradle/actions/setup-gradle@v4 + + - name: Build the debug dev client + if: steps.fingerprint.outputs.live == '0' + working-directory: apps/community-mobile + run: | + set -euo pipefail + npx expo prebuild --platform android --clean --no-install + (cd android && ./gradlew --no-daemon :app:assembleDebug -PreactNativeArchitectures=arm64-v8a) + + - if: steps.fingerprint.outputs.live == '0' + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.fingerprint.outputs.name }} + path: apps/community-mobile/android/app/build/outputs/apk/debug/app-debug.apk + if-no-files-found: error + retention-days: 90 diff --git a/apps/community-mobile/app.config.js b/apps/community-mobile/app.config.js index 1a983daa..012f4d4c 100644 --- a/apps/community-mobile/app.config.js +++ b/apps/community-mobile/app.config.js @@ -107,7 +107,7 @@ module.exports = ({ config }) => ({ name: "civfix", slug: "civfix-community", scheme: "civfix", - version: "1.3.1", + version: "1.4.0", orientation: "portrait", userInterfaceStyle: "automatic", icon: "./assets/icon.png", diff --git a/apps/community-mobile/package.json b/apps/community-mobile/package.json index 9b2b63bd..0fc957bc 100644 --- a/apps/community-mobile/package.json +++ b/apps/community-mobile/package.json @@ -1,6 +1,6 @@ { "name": "community-mobile", - "version": "1.3.1", + "version": "1.4.0", "private": true, "license": "AGPL-3.0-only", "main": "expo-router/entry", @@ -80,6 +80,7 @@ "zustand": "^5.0.1" }, "devDependencies": { + "@expo/fingerprint": "~0.15.5", "@types/react": "~19.1.0", "eslint": "^9.16.0", "eslint-config-expo": "~10.0.0", diff --git a/apps/community-mobile/scripts/native-fingerprint.mjs b/apps/community-mobile/scripts/native-fingerprint.mjs new file mode 100644 index 00000000..36f5d595 --- /dev/null +++ b/apps/community-mobile/scripts/native-fingerprint.mjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node +// Prints the Android native fingerprint of the app: a hash of everything that ends up in the native +// build (dependencies with native code, config plugins, the native parts of app.config.js) and nothing +// that only changes JavaScript. A dev-client APK runs the JavaScript of any branch with the same +// fingerprint, so .github/workflows/android-dev-client.yml names its artifact after this hash and the +// agent box's Android QA installs the artifact matching the branch it tests. Both must compute it the +// same way, which is why it lives here. +// +// The `extra` section and the version fields are skipped: they carry the commit and the Play version +// code, which change on every build without changing any native code. +import { createFingerprintAsync, SourceSkips } from "@expo/fingerprint" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +const appDir = join(dirname(fileURLToPath(import.meta.url)), "..") +const { hash } = await createFingerprintAsync(appDir, { + platforms: ["android"], + sourceSkips: SourceSkips.ExpoConfigExtraSection | SourceSkips.ExpoConfigVersions, +}) +process.stdout.write(`${hash}\n`) diff --git a/apps/community-web/package.json b/apps/community-web/package.json index db77db1d..76a21f19 100644 --- a/apps/community-web/package.json +++ b/apps/community-web/package.json @@ -1,6 +1,6 @@ { "name": "community-web", - "version": "1.3.1", + "version": "1.4.0", "private": true, "license": "AGPL-3.0-only", "description": "civfix public user web app: static-exported Next.js SPA over the civfix API.", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03abf69a..04400689 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,6 +212,9 @@ importers: specifier: ^5.0.1 version: 5.0.15(@types/react@19.1.17)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: + '@expo/fingerprint': + specifier: ~0.15.5 + version: 0.15.5 '@types/react': specifier: ~19.1.0 version: 19.1.17