name: flutter # Analyze + test the Flutter mobile client; build an APK only where # it's actually consumed. dev pushes: analyze+test+codegen only # (~3min) — the operator dev-tests via a local Android Studio build, # not the CI artifact. main pushes: also build a debug APK as the # native-build safety net (Gradle/manifest/plugin breakage that # analyze+test can't catch) before any release tag. tags: signed # release APK. Only runs when the diff touches flutter_client/ or a # shared web input sync_shared.sh copies. on: push: branches: [dev, main] tags: ['v*'] paths: - 'flutter_client/**' - 'web/src/lib/styles/tokens.json' - 'web/src/lib/styles/error-copy.json' - 'web/static/placeholders/album-fallback.svg' - '.gitea/workflows/flutter.yml' pull_request: branches: [main] paths: - 'flutter_client/**' - 'web/src/lib/styles/tokens.json' - 'web/src/lib/styles/error-copy.json' - 'web/static/placeholders/album-fallback.svg' - '.gitea/workflows/flutter.yml' workflow_dispatch: jobs: analyze-test-build: # Toolchain selected via container.image per ci-runners.md # (label = scheduling handle, image = environment). The ci-flutter # image ships Flutter SDK + Android cmdline-tools/platform/build-tools # + Java 25. See ci-requirements.md. runs-on: flutter-ci container: image: git.fabledsword.com/bvandeusen/ci-flutter:3.44 defaults: run: working-directory: flutter_client steps: - name: Checkout uses: actions/checkout@v4 - name: Sync shared assets from web/ run: ./tool/sync_shared.sh - name: Verify generated tokens.dart matches JSON shell: bash run: | dart run tool/gen_tokens.dart if ! git diff --exit-code lib/theme/tokens.dart; then echo "::error::lib/theme/tokens.dart is out of sync with shared/fabledsword.tokens.json" echo "Run: cd flutter_client && dart run tool/gen_tokens.dart" exit 1 fi - name: Pub get run: flutter pub get - name: Codegen (drift, etc.) # build_runner generates *.g.dart files (drift schemas, etc.) # which are gitignored. Must run before analyze + test so the # generated symbols exist. run: dart run build_runner build --delete-conflicting-outputs - name: Analyze run: flutter analyze --fatal-infos - name: Test run: flutter test --reporter compact - name: Build debug APK # main-only: native-build safety net before release tags. # dev skips this (~7min) — operator dev-tests locally. if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: flutter build apk --debug - name: Decode signing keystore if: startsWith(github.ref, 'refs/tags/v') # Reconstructs the release keystore from a base64-encoded # CI secret so every tagged build is signed with the same # key. Without this, each CI runner would generate its own # debug keystore and Android would refuse to upgrade an # existing install (signature mismatch). shell: bash env: ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} run: | if [ -z "${ANDROID_KEYSTORE_B64}" ]; then echo "::error::ANDROID_KEYSTORE_B64 secret is missing — release builds need a consistent signing key" exit 1 fi KEYSTORE_PATH="${RUNNER_TEMP}/minstrel-release.keystore" echo "${ANDROID_KEYSTORE_B64}" | base64 -d > "${KEYSTORE_PATH}" echo "ANDROID_KEYSTORE_PATH=${KEYSTORE_PATH}" >> "${GITHUB_ENV}" - name: Build release APK if: startsWith(github.ref, 'refs/tags/v') # Inject the tag (sans leading 'v') as the build's --build-name # so PackageInfo.version matches what /api/client/version # reports — otherwise the in-app update banner would always # think the user is behind because pubspec.yaml's static # version drifts from the actual release tag. # # ANDROID_KEYSTORE_PATH is set by the previous step; # build.gradle.kts picks it up and signs the release APK with # the operator's persistent keystore. shell: bash env: # PKCS12 keystores collapse store + key password into a single # value, so both env vars map to one secret. build.gradle still # reads them separately to stay format-agnostic. ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} run: | TAG="${GITHUB_REF#refs/tags/v}" flutter build apk --release --build-name="${TAG}" - name: Upload debug APK artifact if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: forgejo/upload-artifact@v3 with: name: minstrel-debug-${{ github.sha }} path: flutter_client/build/app/outputs/flutter-apk/app-debug.apk - name: Attach release APK to release if: startsWith(github.ref, 'refs/tags/v') shell: bash env: CI_TOKEN: ${{ secrets.CI_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" REPO="${GITHUB_REPOSITORY}" # Look up the release ID for this tag. The release object must # exist already — operator creates it (or release.yml will, in # a future enhancement) before pushing the tag. RELEASE_ID=$(curl -fsSL \ -H "Authorization: token ${CI_TOKEN}" \ "https://git.fabledsword.com/api/v1/repos/${REPO}/releases/tags/${TAG}" \ | grep -oP '"id":\s*\K[0-9]+' | head -1) if [ -z "${RELEASE_ID}" ]; then echo "::error::release for tag ${TAG} not found" exit 1 fi curl -fsSL \ -H "Authorization: token ${CI_TOKEN}" \ -F "attachment=@build/app/outputs/flutter-apk/app-release.apk" \ "https://git.fabledsword.com/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=minstrel-${TAG}.apk"