feat: enable Play Store CI deploy via Google Play API

- Add ndk debugSymbolLevel=FULL to release build type (opt-B for debug symbols)
- Add google-api-python-client to Nix devshell
- Add scripts/deploy_playstore.py to upload AAB to internal track
- Add deploy-android-bundle task to Taskfile
- Enable release.yml (remove if:false, wire up task deploy-android-bundle)
- Fix forbidden-files pre-commit hook to run task via nix develop (like dart-check)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-13 17:13:38 +02:00
co-authored by Claude Sonnet 4.6
parent ebd6a27c1a
commit 65aba81952
8 changed files with 89 additions and 35 deletions
+8 -11
View File
@@ -8,11 +8,15 @@ jobs:
deploy-playstore: deploy-playstore:
name: Build & Deploy to Play Store name: Build & Deploy to Play Store
runs-on: self-hosted runs-on: self-hosted
if: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Enable Nix flakes
run: |
mkdir -p ~/.config/nix
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
- name: Prepare Keystore - name: Prepare Keystore
env: env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
@@ -24,15 +28,8 @@ jobs:
exit 1 exit 1
fi fi
- name: Build App Bundle - name: Build & Deploy to Play Store
env: env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
run: nix develop --command task build-android-bundle PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_STORE_CONFIG_JSON }}
run: nix develop --command task deploy-android-bundle
# Play Store upload disabled for now
# - name: Upload to Play Store
# env:
# PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_STORE_CONFIG_JSON }}
# run: |
# echo "$PLAY_STORE_CONFIG_JSON" > play-store-key.json
# # nix develop --command fvm flutter pub run supply ...
+1 -1
View File
@@ -15,7 +15,7 @@ repos:
- id: forbidden-files-hook - id: forbidden-files-hook
name: check for forbidden home-directory files name: check for forbidden home-directory files
language: system language: system
entry: task check-hygiene entry: bash -c 'cd "$(git rev-parse --show-toplevel)" && nix develop --command task check-hygiene'
pass_filenames: false pass_filenames: false
always_run: true always_run: true
- id: dart-check - id: dart-check
+9
View File
@@ -268,6 +268,15 @@ tasks:
cmds: cmds:
- ANDROID_HOME=${ANDROID_HOME:-$HOME/Android/Sdk} fvm flutter build apk --release --no-pub | grep -Ev "was tree-shaken|Tree-shaking can be disabled" - ANDROID_HOME=${ANDROID_HOME:-$HOME/Android/Sdk} fvm flutter build apk --release --no-pub | grep -Ev "was tree-shaken|Tree-shaking can be disabled"
deploy-android-bundle:
desc: Build release AAB and upload to Play Store internal track
deps: [build-android-bundle]
preconditions:
- sh: test -n "$PLAY_STORE_CONFIG_JSON"
msg: "PLAY_STORE_CONFIG_JSON is not set"
cmds:
- python3 scripts/deploy_playstore.py
build-android-bundle: build-android-bundle:
desc: Build a release App Bundle (AAB) desc: Build a release App Bundle (AAB)
deps: [_preflight, _android-sdk-check, _pub-get, generate-changelog] deps: [_preflight, _android-sdk-check, _pub-get, generate-changelog]
+3
View File
@@ -53,6 +53,9 @@ android {
isMinifyEnabled = false isMinifyEnabled = false
isShrinkResources = false isShrinkResources = false
ndk {
debugSymbolLevel = "FULL"
}
} }
} }
} }
+4 -1
View File
@@ -81,7 +81,10 @@
curl curl
jq jq
sqlite sqlite
python3 # used by stalwart-dev/start to pick random ports # python3 base + Google Play API client (for scripts/deploy_playstore.py)
(python3.withPackages (ps: with ps; [
google-api-python-client
])) # used by stalwart-dev/start and deploy_playstore.py
fgj # Codeberg/Forgejo CLI (like gh for GitHub) fgj # Codeberg/Forgejo CLI (like gh for GitHub)
]); ]);
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Upload an Android App Bundle to the Google Play Store internal track."""
import json
import os
import sys
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
PACKAGE_NAME = "de.sharedinbox.mua"
AAB_PATH = "build/app/outputs/bundle/release/app-release.aab"
TRACK = "internal"
def main():
config_json = os.environ.get("PLAY_STORE_CONFIG_JSON")
if not config_json:
print("Error: PLAY_STORE_CONFIG_JSON environment variable not set", file=sys.stderr)
sys.exit(1)
if not os.path.exists(AAB_PATH):
print(f"Error: AAB not found at {AAB_PATH}", file=sys.stderr)
sys.exit(1)
creds = service_account.Credentials.from_service_account_info(
json.loads(config_json),
scopes=["https://www.googleapis.com/auth/androidpublisher"],
)
service = build("androidpublisher", "v3", credentials=creds)
edit = service.edits().insert(body={}, packageName=PACKAGE_NAME).execute()
edit_id = edit["id"]
media = MediaFileUpload(AAB_PATH, mimetype="application/octet-stream", resumable=True)
bundle = (
service.edits()
.bundles()
.upload(packageName=PACKAGE_NAME, editId=edit_id, media_body=media)
.execute()
)
version_code = bundle["versionCode"]
print(f"Uploaded AAB, version code: {version_code}")
service.edits().tracks().update(
packageName=PACKAGE_NAME,
editId=edit_id,
track=TRACK,
body={"releases": [{"versionCodes": [version_code], "status": "completed"}]},
).execute()
service.edits().commit(packageName=PACKAGE_NAME, editId=edit_id).execute()
print(f"Deployed version {version_code} to {TRACK} track")
if __name__ == "__main__":
main()
-21
View File
@@ -1,21 +0,0 @@
#!/bin/bash
# Helper script to generate a production Android Keystore.
# Run this from the project root.
# Define the path relative to the project root
KEYSTORE_PATH="android/app/upload-keystore.jks"
echo "Generating keystore at: $KEYSTORE_PATH"
keytool -genkey -v \
-keystore "$KEYSTORE_PATH" \
-alias upload \
-keyalg RSA \
-keysize 2048 \
-validity 10000
echo ""
echo "Done."
echo "1. Remember your password!"
echo "2. Back up $KEYSTORE_PATH safely."
echo "3. Do NOT commit the .jks file or passwords to git."
+5 -1
View File
@@ -12,7 +12,7 @@ Data Protection blabla page!
* **Taskfile:** Added `task build-android-bundle` to generate the `.aab` file. * **Taskfile:** Added `task build-android-bundle` to generate the `.aab` file.
* **CI Workflow:** Created `.forgejo/workflows/release.yml` which triggers on merge to `main`. * **CI Workflow:** Created `.forgejo/workflows/release.yml` which triggers on merge to `main`.
## 2. What you need to do next
### A. Create the Keystore ### A. Create the Keystore
Run the helper script I created for you: Run the helper script I created for you:
```bash ```bash
@@ -26,6 +26,7 @@ Go to **Settings > Actions > Secrets** in your Codeberg repo and add:
2. **`ANDROID_KEYSTORE_PASSWORD`**: Your keystore password. 2. **`ANDROID_KEYSTORE_PASSWORD`**: Your keystore password.
3. **`PLAY_STORE_CONFIG_JSON`**: The JSON key from your Google Play Service Account. 3. **`PLAY_STORE_CONFIG_JSON`**: The JSON key from your Google Play Service Account.
### C. First Manual Upload ### C. First Manual Upload
Google Play requires the **very first upload** to be done manually through the web console: Google Play requires the **very first upload** to be done manually through the web console:
1. Generate your keystore using `./t.sh`. 1. Generate your keystore using `./t.sh`.
@@ -37,6 +38,9 @@ Google Play requires the **very first upload** to be done manually through the w
3. Upload the resulting `.aab` from `build/app/outputs/bundle/release/app-release.aab` to the Play Console (Internal Testing or Production track). 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. 4. This "locks in" your signing key.
## 2. What you need to do next
## 3. Firebase Test Lab ## 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: Once you have the Service Account JSON, you can add a task to `Taskfile.yml` to run automated tests on real devices:
```yaml ```yaml