Skip to content

Commit 4a85572

Browse files
committed
[action]: release aab to play store dev console;
1 parent 691eaea commit 4a85572

File tree

1 file changed

+166
-7
lines changed

1 file changed

+166
-7
lines changed

.github/workflows/build-release.yml

Lines changed: 166 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,197 @@ on:
77
pull_request:
88
branches:
99
- main
10+
release:
11+
types:
12+
- published
1013

1114
jobs:
1215
build:
1316
runs-on: ubuntu-latest
14-
17+
env:
18+
PACKAGE_NAME: com.mua.prayertracker
19+
PLAY_TRACK: production
20+
1521
steps:
1622
- name: Checkout code
1723
uses: actions/checkout@v4
18-
24+
1925
- name: Set up JDK 17
2026
uses: actions/setup-java@v4
2127
with:
2228
distribution: 'temurin'
2329
java-version: '17'
24-
30+
2531
- name: Setup Gradle
2632
uses: gradle/actions/setup-gradle@v3
27-
33+
2834
- name: Decode and save keystore
2935
run: |
3036
mkdir -p secret
3137
echo "${{ secrets.PRAYER_TRACKER_JKS }}" | base64 -d > secret/key-store.jks
32-
38+
3339
- name: Clean project
3440
run: ./gradlew clean
35-
41+
3642
- name: Build release bundle
3743
run: |
3844
./gradlew bundleRelease \
3945
-PPRAYER_TRACKER_STORE_PASSWORD="${{ secrets.PRAYER_TRACKER_STORE_PASSWORD }}" \
4046
-PPRAYER_TRACKER_KEY_ALIAS="${{ secrets.PRAYER_TRACKER_KEY_ALIAS }}" \
4147
-PPRAYER_TRACKER_KEY_PASSWORD="${{ secrets.PRAYER_TRACKER_KEY_PASSWORD }}"
42-
48+
4349
- name: Verify bundle signature
4450
run: jarsigner -verify -verbose -certs app/build/outputs/bundle/release/app-release.aab
51+
52+
- name: Decode Play service account (release only)
53+
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
54+
env:
55+
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PRAYER_TRACKER_PLAY_SERVICE_ACCOUNT_JSON }}
56+
run: |
57+
if [ -z "$PLAY_SERVICE_ACCOUNT_JSON" ]; then
58+
echo "Missing secret: PRAYER_TRACKER_PLAY_SERVICE_ACCOUNT_JSON"
59+
exit 1
60+
fi
61+
62+
mkdir -p secret
63+
python3 - <<'PY'
64+
import base64
65+
import json
66+
import os
67+
from pathlib import Path
68+
69+
raw = os.environ["PLAY_SERVICE_ACCOUNT_JSON"].strip()
70+
output_path = Path("secret/play-service-account.json")
71+
72+
try:
73+
parsed = json.loads(raw)
74+
output_path.write_text(json.dumps(parsed), encoding="utf-8")
75+
except json.JSONDecodeError:
76+
decoded = base64.b64decode(raw).decode("utf-8")
77+
parsed = json.loads(decoded)
78+
output_path.write_text(json.dumps(parsed), encoding="utf-8")
79+
PY
80+
81+
- name: Publish to Google Play (release only)
82+
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
83+
run: |
84+
set -euo pipefail
85+
86+
AAB_PATH="app/build/outputs/bundle/release/app-release.aab"
87+
SERVICE_ACCOUNT_FILE="secret/play-service-account.json"
88+
89+
if [ ! -f "$AAB_PATH" ]; then
90+
echo "AAB not found at $AAB_PATH"
91+
exit 1
92+
fi
93+
94+
if [ ! -f "$SERVICE_ACCOUNT_FILE" ]; then
95+
echo "Service account file not found at $SERVICE_ACCOUNT_FILE"
96+
exit 1
97+
fi
98+
99+
# Build and sign a JWT assertion for OAuth 2.0 service-account flow.
100+
python3 - <<'PY'
101+
import base64
102+
import json
103+
import time
104+
from pathlib import Path
105+
106+
service_account = json.loads(Path("secret/play-service-account.json").read_text(encoding="utf-8"))
107+
now = int(time.time())
108+
109+
def b64url(data: bytes) -> str:
110+
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
111+
112+
header = {"alg": "RS256", "typ": "JWT"}
113+
payload = {
114+
"iss": service_account["client_email"],
115+
"scope": "https://www.googleapis.com/auth/androidpublisher",
116+
"aud": "https://oauth2.googleapis.com/token",
117+
"iat": now,
118+
"exp": now + 3600,
119+
}
120+
121+
unsigned_jwt = (
122+
f"{b64url(json.dumps(header, separators=(',', ':')).encode('utf-8'))}."
123+
f"{b64url(json.dumps(payload, separators=(',', ':')).encode('utf-8'))}"
124+
)
125+
126+
Path("secret/unsigned-jwt.txt").write_text(unsigned_jwt, encoding="utf-8")
127+
Path("secret/service-account-private-key.pem").write_text(
128+
service_account["private_key"],
129+
encoding="utf-8",
130+
)
131+
PY
132+
133+
UNSIGNED_JWT="$(cat secret/unsigned-jwt.txt)"
134+
JWT_SIGNATURE="$(printf '%s' "$UNSIGNED_JWT" | openssl dgst -sha256 -sign secret/service-account-private-key.pem | openssl base64 -A | tr '+/' '-_' | tr -d '=')"
135+
JWT_ASSERTION="$UNSIGNED_JWT.$JWT_SIGNATURE"
136+
137+
TOKEN_RESPONSE="$(curl -sS --fail-with-body -X POST \
138+
-H "Content-Type: application/x-www-form-urlencoded" \
139+
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
140+
--data-urlencode "assertion=$JWT_ASSERTION" \
141+
https://oauth2.googleapis.com/token)"
142+
143+
ACCESS_TOKEN="$(printf '%s' "$TOKEN_RESPONSE" | python3 -c 'import json,sys; print(json.loads(sys.stdin.read()).get("access_token", ""))')"
144+
if [ -z "$ACCESS_TOKEN" ]; then
145+
echo "Failed to obtain Google OAuth access token"
146+
exit 1
147+
fi
148+
149+
CREATE_EDIT_RESPONSE="$(curl -sS --fail-with-body -X POST \
150+
-H "Authorization: Bearer $ACCESS_TOKEN" \
151+
-H "Content-Type: application/json" \
152+
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits")"
153+
154+
EDIT_ID="$(printf '%s' "$CREATE_EDIT_RESPONSE" | python3 -c 'import json,sys; print(json.loads(sys.stdin.read()).get("id", ""))')"
155+
if [ -z "$EDIT_ID" ]; then
156+
echo "Failed to create Play edit"
157+
exit 1
158+
fi
159+
160+
UPLOAD_RESPONSE="$(curl -sS --fail-with-body -X POST \
161+
-H "Authorization: Bearer $ACCESS_TOKEN" \
162+
-H "Content-Type: application/octet-stream" \
163+
--data-binary "@$AAB_PATH" \
164+
"https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID/bundles?uploadType=media")"
165+
166+
VERSION_CODE="$(printf '%s' "$UPLOAD_RESPONSE" | python3 -c 'import json,sys; print(json.loads(sys.stdin.read()).get("versionCode", ""))')"
167+
if [ -z "$VERSION_CODE" ]; then
168+
echo "Failed to upload bundle to Play edit"
169+
exit 1
170+
fi
171+
export VERSION_CODE
172+
173+
TRACK_PAYLOAD="$(python3 - <<'PY'
174+
import json
175+
import os
176+
177+
print(
178+
json.dumps(
179+
{
180+
"releases": [
181+
{
182+
"status": "completed",
183+
"versionCodes": [os.environ["VERSION_CODE"]],
184+
}
185+
]
186+
}
187+
)
188+
)
189+
PY
190+
)"
191+
192+
curl -sS --fail-with-body -X PUT \
193+
-H "Authorization: Bearer $ACCESS_TOKEN" \
194+
-H "Content-Type: application/json" \
195+
-d "$TRACK_PAYLOAD" \
196+
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID/tracks/$PLAY_TRACK" >/dev/null
197+
198+
curl -sS --fail-with-body -X POST \
199+
-H "Authorization: Bearer $ACCESS_TOKEN" \
200+
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID:commit" >/dev/null
201+
202+
# Best effort cleanup for generated credential material.
203+
rm -f secret/unsigned-jwt.txt secret/service-account-private-key.pem secret/play-service-account.json

0 commit comments

Comments
 (0)