Skip to content

Commit e141fed

Browse files
committed
ci: add android play internal deploy
1 parent e2bd60d commit e141fed

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: Android Play Internal Deploy
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
release_notes:
7+
description: Optional Google Play release notes for en-US.
8+
required: false
9+
type: string
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
deploy_internal:
16+
name: Build and upload internal testing draft
17+
runs-on: ubuntu-latest
18+
environment: release
19+
env:
20+
ANDROID_KEYSTORE_PATH: ${{ runner.temp }}/ontime-upload.jks
21+
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
22+
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
23+
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
24+
REST_API_URL: ${{ vars.REST_API_URL }}
25+
26+
steps:
27+
- name: Require main branch
28+
run: |
29+
if [ "$GITHUB_REF" != "refs/heads/main" ]; then
30+
echo "Android Play Internal Deploy must be run from main." >&2
31+
exit 1
32+
fi
33+
34+
- uses: actions/checkout@v4
35+
36+
- uses: actions/setup-java@v4
37+
with:
38+
distribution: temurin
39+
java-version: "17"
40+
41+
- uses: subosito/flutter-action@v2
42+
with:
43+
flutter-version: "3.32.6"
44+
channel: stable
45+
cache: true
46+
47+
- name: Install native test dependencies
48+
run: sudo apt-get update && sudo apt-get install -y sqlite3 libsqlite3-dev
49+
50+
- name: Validate release configuration
51+
env:
52+
ANDROID_GOOGLE_SERVICES_JSON_B64: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON_B64 }}
53+
ANDROID_UPLOAD_KEYSTORE_B64: ${{ secrets.ANDROID_UPLOAD_KEYSTORE_B64 }}
54+
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
55+
run: |
56+
missing=0
57+
for name in \
58+
ANDROID_GOOGLE_SERVICES_JSON_B64 \
59+
ANDROID_UPLOAD_KEYSTORE_B64 \
60+
ANDROID_KEYSTORE_PASSWORD \
61+
ANDROID_KEY_ALIAS \
62+
ANDROID_KEY_PASSWORD \
63+
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON \
64+
REST_API_URL
65+
do
66+
if [ -z "${!name}" ]; then
67+
echo "$name is required for Android Play internal deploy." >&2
68+
missing=1
69+
fi
70+
done
71+
exit "$missing"
72+
73+
- name: Decode Android Firebase config
74+
env:
75+
ANDROID_GOOGLE_SERVICES_JSON_B64: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON_B64 }}
76+
run: |
77+
mkdir -p android/app/src/release
78+
printf '%s' "$ANDROID_GOOGLE_SERVICES_JSON_B64" | base64 --decode > android/app/src/release/google-services.json
79+
test -s android/app/src/release/google-services.json
80+
81+
- name: Decode Android upload keystore
82+
env:
83+
ANDROID_UPLOAD_KEYSTORE_B64: ${{ secrets.ANDROID_UPLOAD_KEYSTORE_B64 }}
84+
run: |
85+
printf '%s' "$ANDROID_UPLOAD_KEYSTORE_B64" | base64 --decode > "$ANDROID_KEYSTORE_PATH"
86+
chmod 600 "$ANDROID_KEYSTORE_PATH"
87+
test -s "$ANDROID_KEYSTORE_PATH"
88+
89+
- name: Install Flutter packages
90+
run: flutter pub get
91+
92+
- name: Run code generation
93+
run: dart run build_runner build --delete-conflicting-outputs
94+
95+
- name: Verify generated files are current
96+
run: git diff --exit-code
97+
98+
- name: Analyze
99+
run: flutter analyze
100+
101+
- name: Test
102+
run: flutter test
103+
104+
- name: Build Android app bundle
105+
run: flutter build appbundle --release --dart-define=REST_API_URL="$REST_API_URL"
106+
107+
- name: Prepare Play release notes
108+
if: ${{ inputs.release_notes != '' }}
109+
env:
110+
RELEASE_NOTES: ${{ inputs.release_notes }}
111+
run: |
112+
mkdir -p distribution/whatsnew
113+
printf '%s\n' "$RELEASE_NOTES" > distribution/whatsnew/whatsnew-en-US
114+
115+
- name: Upload signed app bundle artifact
116+
uses: actions/upload-artifact@v4
117+
with:
118+
name: ontime-android-release-aab
119+
path: build/app/outputs/bundle/release/app-release.aab
120+
if-no-files-found: error
121+
retention-days: 14
122+
123+
- name: Upload internal testing draft with release notes
124+
if: ${{ inputs.release_notes != '' }}
125+
uses: r0adkll/upload-google-play@v1
126+
with:
127+
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
128+
packageName: club.devkor.ontime
129+
releaseFiles: build/app/outputs/bundle/release/app-release.aab
130+
tracks: internal
131+
status: draft
132+
whatsNewDirectory: distribution/whatsnew
133+
134+
- name: Upload internal testing draft
135+
if: ${{ inputs.release_notes == '' }}
136+
uses: r0adkll/upload-google-play@v1
137+
with:
138+
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
139+
packageName: club.devkor.ontime
140+
releaseFiles: build/app/outputs/bundle/release/app-release.aab
141+
tracks: internal
142+
status: draft

docs/Android-Release-Configuration.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@ Android release builds must include the production Firebase client config so Fir
88

99
The release verification workflow decodes this secret to `android/app/src/release/google-services.json` before running the Android release build. Generated `google-services.json` files are ignored and must not be committed.
1010

11+
## Google Play Internal Testing Deploy
12+
13+
Use the `Android Play Internal Deploy` GitHub Actions workflow to build a signed
14+
Android App Bundle and upload it to the Google Play Internal Testing track as a
15+
draft release. The workflow is manual-only and must be dispatched from `main`.
16+
17+
Configure a GitHub environment named `release` with required reviewers before
18+
using the workflow. Store these environment secrets there:
19+
20+
- `ANDROID_GOOGLE_SERVICES_JSON_B64`: base64-encoded production Android
21+
Firebase config.
22+
- `ANDROID_UPLOAD_KEYSTORE_B64`: base64-encoded Google Play upload keystore.
23+
- `ANDROID_KEYSTORE_PASSWORD`: upload keystore password.
24+
- `ANDROID_KEY_ALIAS`: upload key alias.
25+
- `ANDROID_KEY_PASSWORD`: upload key password.
26+
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`: raw service-account JSON for the Google
27+
Play Developer API.
28+
29+
Store this environment variable in `release`:
30+
31+
- `REST_API_URL`: production API base URL passed to Flutter with
32+
`--dart-define`.
33+
34+
The deploy workflow runs package install, code generation, generated-file drift
35+
checking, analysis, tests, and then `flutter build appbundle --release`. It
36+
uploads the signed `.aab` as a 14-day GitHub Actions artifact and creates a
37+
draft release on Google Play Internal Testing. If the optional release notes
38+
input is empty, the workflow uploads without custom release notes.
39+
40+
`pubspec.yaml` remains the source of truth for `version: major.minor.patch+build`.
41+
Before dispatching the workflow, bump the build number so it is greater than
42+
every previously uploaded Google Play build for `club.devkor.ontime`. Duplicate
43+
build numbers fail during the Google Play upload step.
44+
1145
## Local Release Build
1246

1347
Create the release source-set config before building:

docs/Android-Signing-Setup.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ The Gradle build also accepts the legacy `ONTIME_ANDROID_KEYSTORE_PATH`,
5757
`ONTIME_ANDROID_KEYSTORE_PASSWORD`, `ONTIME_ANDROID_KEY_ALIAS`, and
5858
`ONTIME_ANDROID_KEY_PASSWORD` variable names for compatibility.
5959

60+
## Configure CI Release Signing
61+
62+
For GitHub Actions deploys, base64-encode the upload keystore and store it as
63+
the `ANDROID_UPLOAD_KEYSTORE_B64` secret in the protected `release`
64+
environment:
65+
66+
```sh
67+
base64 -i ~/secure/ontime-upload.jks | pbcopy
68+
```
69+
70+
On Linux, use:
71+
72+
```sh
73+
base64 -w 0 ~/secure/ontime-upload.jks
74+
```
75+
76+
The deploy workflow decodes this secret into `$RUNNER_TEMP/ontime-upload.jks`
77+
and exports `ANDROID_KEYSTORE_PATH` to that temporary file. Do not create
78+
`android/key.properties` in CI.
79+
6080
## Build a Signed Release
6181

6282
Google Play prefers Android App Bundles:

0 commit comments

Comments
 (0)