From 0cefc8f8e7e58fb581e8bfbf079f7f72b6399b8e Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Fri, 5 Jun 2026 09:00:26 +0200 Subject: [PATCH] load android signing secrets from SOPS for local builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keystore is decoded into /dev/shm (tmpfs, RAM-only) during the build and cleaned up on exit — never written to physical disk. ANDROID_KEYSTORE_PATH is now required with no fallback; missing it fails loudly. Dagger CI path updated to write to /tmp and set ANDROID_KEYSTORE_PATH accordingly. Also fix check_ci_images.sh: filter out incomplete image tags ending in ':' that arise from dynamic From("image:"+variable) concatenations. Co-Authored-By: Claude Sonnet 4.6 --- Taskfile.yml | 9 ++--- android/app/build.gradle.kts | 13 +----- ci/main.go | 3 +- scripts/build_android_bundle_local.sh | 15 +++++++ scripts/check_ci_images.sh | 2 +- to-playstore.md | 57 --------------------------- 6 files changed, 24 insertions(+), 75 deletions(-) create mode 100755 scripts/build_android_bundle_local.sh delete mode 100644 to-playstore.md diff --git a/Taskfile.yml b/Taskfile.yml index ad944f8..b39632d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -544,15 +544,14 @@ tasks: deploy-android-bundle: desc: Build release AAB and upload to Play Store internal track (local/fvm) deps: [build-android-bundle-local] - preconditions: - - sh: test -n "$PLAY_STORE_CONFIG_JSON" - msg: "PLAY_STORE_CONFIG_JSON is not set" + dotenv: [".env"] cmds: - - python3 scripts/deploy_playstore.py + - sops exec-env secrets.enc.yaml 'python3 scripts/deploy_playstore.py' build-android-bundle-local: desc: Build a release App Bundle (AAB) locally via fvm (not Dagger) deps: [_preflight, _android-sdk-check, _codegen, generate-changelog] + dotenv: [".env"] method: timestamp sources: - lib/**/*.dart @@ -561,7 +560,7 @@ tasks: generates: - build/app/outputs/bundle/release/app-release.aab cmds: - - ANDROID_HOME=${ANDROID_HOME:-$HOME/Android/Sdk} fvm flutter build appbundle --release --no-pub --build-number $(date +%s) --build-name $(date +%y%m%d-%H%M) --dart-define=GIT_HASH=$(git rev-parse --short HEAD) | grep -Ev "was tree-shaken|Tree-shaking can be disabled" + - sops exec-env secrets.enc.yaml 'bash scripts/build_android_bundle_local.sh' deploy-android: desc: Build release APK and upload via scp to $ANDROID_APK_SCP_USER@$ANDROID_APK_SCP_HOST:$ANDROID_APK_SCP_PATH diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index eba2aad..15eb163 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -24,13 +24,11 @@ android { signingConfigs { create("release") { - // Hardcoded alias matching t.sh keyAlias = "upload" - // Use the same password for both key and keystore val pass = System.getenv("ANDROID_KEYSTORE_PASSWORD") storePassword = pass keyPassword = pass - storeFile = file("upload-keystore.jks") + storeFile = file(System.getenv("ANDROID_KEYSTORE_PATH") ?: error("ANDROID_KEYSTORE_PATH is not set")) } } @@ -46,14 +44,7 @@ android { buildTypes { release { - // Use the signing config defined above for release builds. - // If the keystore file exists (e.g. in CI or manually placed), sign it. - signingConfig = if (signingConfigs.getByName("release").storeFile?.exists() == true) { - signingConfigs.getByName("release") - } else { - signingConfigs.getByName("debug") - } - + signingConfig = signingConfigs.getByName("release") isMinifyEnabled = false isShrinkResources = false ndk { diff --git a/ci/main.go b/ci/main.go index 0c9f7b0..920cc44 100644 --- a/ci/main.go +++ b/ci/main.go @@ -687,7 +687,8 @@ func (m *Ci) setupKeystore(keystoreBase64 *dagger.Secret, keystorePassword *dagg return m.androidBase(). WithSecretVariable("ANDROID_KEYSTORE_BASE64", keystoreBase64). WithSecretVariable("ANDROID_KEYSTORE_PASSWORD", keystorePassword). - WithExec([]string{"/bin/sh", "-c", `echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks`}) + WithExec([]string{"/bin/sh", "-c", `echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > /tmp/upload-keystore.jks`}). + WithEnvVariable("ANDROID_KEYSTORE_PATH", "/tmp/upload-keystore.jks") } // BuildAndroidApk builds a release APK signed with the upload key. diff --git a/scripts/build_android_bundle_local.sh b/scripts/build_android_bundle_local.sh new file mode 100755 index 0000000..4ebc424 --- /dev/null +++ b/scripts/build_android_bundle_local.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +tmp=$(mktemp /dev/shm/keystore.XXXXXX.jks) +trap "rm -f $tmp" EXIT + +printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$tmp" + +ANDROID_KEYSTORE_PATH="$tmp" \ +ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}" \ +fvm flutter build appbundle --release --no-pub \ + --build-number "$(date +%s)" \ + --build-name "$(date +%y%m%d-%H%M)" \ + --dart-define="GIT_HASH=$(git rev-parse --short HEAD)" \ + | grep -Ev "was tree-shaken|Tree-shaking can be disabled" diff --git a/scripts/check_ci_images.sh b/scripts/check_ci_images.sh index 6ae3d97..56645ef 100755 --- a/scripts/check_ci_images.sh +++ b/scripts/check_ci_images.sh @@ -7,7 +7,7 @@ ROOT=$(git rev-parse --show-toplevel) FILE="$ROOT/ci/main.go" # Static images from From("...") literals in ci/main.go -static_images=$(grep -oP 'From\("\K[^"]+' "$FILE" | sort -u) +static_images=$(grep -oP 'From\("\K[^"]+' "$FILE" | grep -v ':$' | sort -u) # Dynamic Flutter image derived from .fvmrc (not a literal in main.go) FVMRC="$ROOT/.fvmrc" diff --git a/to-playstore.md b/to-playstore.md deleted file mode 100644 index a7aa25a..0000000 --- a/to-playstore.md +++ /dev/null @@ -1,57 +0,0 @@ -# Play Store Publishing Roadmap - -To publish the Flutter app to the Play Store, you need to transition from a "development" state to a "production-ready" state. - -Data Protection blabla page! - -## 1. What has been done -* **Application ID:** Changed to `de.sharedinbox.mua` (verified in `build.gradle.kts`, `MainActivity.kt`, and integration tests). -* **Build Logic:** `android/app/build.gradle.kts` now supports: - * **Local builds:** Using `key.properties` (ignored by git). - * **CI builds:** Using environment variables (`ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`, `ANDROID_KEYSTORE_PASSWORD`). -* **Taskfile:** Added `task build-android-bundle` to generate the `.aab` file. -* **CI Workflow:** Created `.forgejo/workflows/release.yml` which triggers on merge to `main`. - - -### A. Create the Keystore -Run the helper script I created for you: -```bash -./t.sh -``` -Follow the prompts and use a strong password (24-32 chars). - -### B. Configure Codeberg Secrets -Go to **Settings > Actions > Secrets** in your Codeberg repo and add: -1. **`ANDROID_KEYSTORE_BASE64`**: The output of `base64 -w 0 android/app/upload-keystore.jks`. -2. **`ANDROID_KEYSTORE_PASSWORD`**: Your keystore password. -3. **`PLAY_STORE_CONFIG_JSON`**: The JSON key from your Google Play Service Account. - - -### C. First Manual Upload -Google Play requires the **very first upload** to be done manually through the web console: -1. Generate your keystore using `./t.sh`. -2. Run the build locally using temporary environment variables: - ```bash - export ANDROID_KEYSTORE_PASSWORD=your_password - nix develop --command task build-android-bundle - ``` -3. Upload the resulting `.aab` from `build/app/outputs/bundle/release/app-release.aab` to the Play Console (Internal Testing or Production track). -4. This "locks in" your signing key. - -## 2. What you need to do next - - -## 3. Firebase Test Lab -Once you have the Service Account JSON, you can add a task to `Taskfile.yml` to run automated tests on real devices: -```yaml -test-lab: - desc: Run integration tests in Firebase Test Lab - cmds: - - gcloud firebase test android run \ - --type instrumentation \ - --app build/app/outputs/apk/debug/app-debug.apk \ - --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \ - --device model=virtuall1,version=30 -``` - -**Recommendation:** Complete step **A** (Keystore) and **B** (Secrets) first. Once the first manual upload is done, the CI will take over for all future merges to `main`.