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:
co-authored by
Claude Sonnet 4.6
parent
ebd6a27c1a
commit
65aba81952
@@ -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 ...
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ android {
|
|||||||
|
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
isShrinkResources = false
|
isShrinkResources = false
|
||||||
|
ndk {
|
||||||
|
debugSymbolLevel = "FULL"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Executable
+59
@@ -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()
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user