From 2bcfbf89d3568eac5ab287ef92ea966f54c3ed96 Mon Sep 17 00:00:00 2001 From: Koen J Date: Thu, 19 Feb 2026 11:13:27 +0100 Subject: [PATCH] Added proper automation for playstore builds. --- .gitlab-ci.yml | 41 +++--- deploy-playstore.sh => build-playstore.sh | 17 ++- deploy-stable.sh | 47 +++---- deploy-unstable.sh | 37 +++--- publish_playstore.py | 155 ++++++++++++++++++++++ venv-playstore.sh | 13 ++ 6 files changed, 246 insertions(+), 64 deletions(-) rename deploy-playstore.sh => build-playstore.sh (76%) create mode 100644 publish_playstore.py create mode 100644 venv-playstore.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 66250dfb..dec0b88d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,20 +1,17 @@ -variables: - GIT_SUBMODULE_STRATEGY: recursive - -stages: -- buildAndDeployApkUnstable -- buildAndDeployApkStable -- buildAndDeployPlaystore - buildAndDeployApkUnstable: stage: buildAndDeployApkUnstable script: - sh deploy-unstable.sh only: - tags - except: - - ^(dev) when: manual + needs: [] + allow_failure: true + artifacts: + when: always + expire_in: 30 days + paths: + - app/build/outputs/apk/unstable/release/*.apk buildAndDeployApkStable: stage: buildAndDeployApkStable @@ -22,16 +19,28 @@ buildAndDeployApkStable: - sh deploy-stable.sh only: - tags - except: - - branches when: manual + needs: [] + artifacts: + when: always + expire_in: 30 days + paths: + - app/build/outputs/apk/stable/release/*.apk buildAndDeployPlaystore: stage: buildAndDeployPlaystore script: - - sh deploy-playstore.sh + - sh build-playstore.sh + - bash tools/venv_playstore.sh + - . .venv-playstore/bin/activate + - python publish_playstore.py --sa /root/grayjay.json --package com.futo.platformplayer.playstore --aab ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab --track production --status completed only: - tags - except: - - branches - when: manual + when: on_success + needs: + - buildAndDeployApkStable + artifacts: + when: always + expire_in: 30 days + paths: + - app/build/outputs/bundle/playstoreRelease/*.aab diff --git a/deploy-playstore.sh b/build-playstore.sh similarity index 76% rename from deploy-playstore.sh rename to build-playstore.sh index 498abfa3..97454407 100644 --- a/deploy-playstore.sh +++ b/build-playstore.sh @@ -1,5 +1,13 @@ #!/bin/sh +set -eu + DOCUMENT_ROOT=/var/www/html +MAINT_FILE="$DOCUMENT_ROOT/maintenance.file" + +cleanup() { + rm -f "$MAINT_FILE" +} +trap cleanup EXIT INT TERM # Sign sources echo "Signing all sources..." @@ -11,12 +19,12 @@ echo "Building content..." # Take site offline echo "Taking site offline..." -touch $DOCUMENT_ROOT/maintenance.file +touch "$MAINT_FILE" # Swap over the content echo "Deploying content..." -cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab $DOCUMENT_ROOT/app-playstore-release.aab -aws s3 cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab s3://artifacts-grayjay-app/app-playstore-release.aab +cp ./app/build/outputs/bundle/playstoreRelease/app-playstore-release.aab \ + "$DOCUMENT_ROOT/app-playstore-release.aab" # Notify Cloudflare to wipe the CDN cache echo "Purging Cloudflare cache for zone $CLOUDFLARE_ZONE_ID..." @@ -29,4 +37,5 @@ sleep 30 # Take site back online echo "Bringing site back online..." -rm $DOCUMENT_ROOT/maintenance.file +rm -f "$MAINT_FILE" +trap - EXIT INT TERM diff --git a/deploy-stable.sh b/deploy-stable.sh index 09de7833..126952ef 100644 --- a/deploy-stable.sh +++ b/deploy-stable.sh @@ -1,5 +1,13 @@ #!/bin/sh +set -eu + DOCUMENT_ROOT=/var/www/html +MAINT_FILE="$DOCUMENT_ROOT/maintenance.file" + +cleanup() { + rm -f "$MAINT_FILE" +} +trap cleanup EXIT INT TERM # Sign sources echo "Signing all sources..." @@ -11,35 +19,21 @@ echo "Building content..." # Take site offline echo "Taking site offline..." -touch $DOCUMENT_ROOT/maintenance.file +touch "$MAINT_FILE" # Swap over the content echo "Deploying content..." -cp ./app/build/outputs/apk/stable/release/app-stable-x86_64-release.apk $DOCUMENT_ROOT/app-x86_64-release.apk -cp ./app/build/outputs/apk/stable/release/app-stable-arm64-v8a-release.apk $DOCUMENT_ROOT/app-arm64-v8a-release.apk -cp ./app/build/outputs/apk/stable/release/app-stable-armeabi-v7a-release.apk $DOCUMENT_ROOT/app-armeabi-v7a-release.apk -cp ./app/build/outputs/apk/stable/release/app-stable-universal-release.apk $DOCUMENT_ROOT/app-universal-release.apk -cp ./app/build/outputs/apk/stable/release/app-stable-x86-release.apk $DOCUMENT_ROOT/app-x86-release.apk -cp ./app/build/outputs/apk/stable/release/app-stable-arm64-v8a-release.apk $DOCUMENT_ROOT/app-release.apk +cp ./app/build/outputs/apk/stable/release/app-stable-x86_64-release.apk "$DOCUMENT_ROOT/app-x86_64-release.apk" +cp ./app/build/outputs/apk/stable/release/app-stable-arm64-v8a-release.apk "$DOCUMENT_ROOT/app-arm64-v8a-release.apk" +cp ./app/build/outputs/apk/stable/release/app-stable-armeabi-v7a-release.apk "$DOCUMENT_ROOT/app-armeabi-v7a-release.apk" +cp ./app/build/outputs/apk/stable/release/app-stable-universal-release.apk "$DOCUMENT_ROOT/app-universal-release.apk" +cp ./app/build/outputs/apk/stable/release/app-stable-x86-release.apk "$DOCUMENT_ROOT/app-x86-release.apk" +cp ./app/build/outputs/apk/stable/release/app-stable-arm64-v8a-release.apk "$DOCUMENT_ROOT/app-release.apk" -VERSION=$(git describe --tags) -echo $VERSION > $DOCUMENT_ROOT/version.txt -mkdir -p $DOCUMENT_ROOT/changelogs -git tag -l --format='%(contents)' $VERSION > $DOCUMENT_ROOT/changelogs/$VERSION - -aws s3 cp ./app/build/outputs/apk/stable/release/app-stable-x86_64-release.apk s3://artifacts-grayjay-app/app-x86_64-release.apk -aws s3 cp ./app/build/outputs/apk/stable/release/app-stable-arm64-v8a-release.apk s3://artifacts-grayjay-app/app-arm64-v8a-release.apk -aws s3 cp ./app/build/outputs/apk/stable/release/app-stable-armeabi-v7a-release.apk s3://artifacts-grayjay-app/app-armeabi-v7a-release.apk -aws s3 cp ./app/build/outputs/apk/stable/release/app-stable-universal-release.apk s3://artifacts-grayjay-app/app-universal-release.apk -aws s3 cp ./app/build/outputs/apk/stable/release/app-stable-x86-release.apk s3://artifacts-grayjay-app/app-x86-release.apk -aws s3 cp ./app/build/outputs/apk/stable/release/app-stable-arm64-v8a-release.apk s3://artifacts-grayjay-app/app-release.apk - -VERSION=$(git describe --tags) -echo $VERSION > ./version.txt -git tag -l --format='%(contents)' $VERSION > ./changelog.txt - -aws s3 cp ./version.txt s3://artifacts-grayjay-app/version.txt -aws s3 cp ./changelog.txt s3://artifacts-grayjay-app/changelogs/$VERSION +VERSION="$(git describe --tags)" +echo "$VERSION" > "$DOCUMENT_ROOT/version.txt" +mkdir -p "$DOCUMENT_ROOT/changelogs" +git tag -l --format='%(contents)' "$VERSION" > "$DOCUMENT_ROOT/changelogs/$VERSION" # Notify Cloudflare to wipe the CDN cache echo "Purging Cloudflare cache for zone $CLOUDFLARE_ZONE_ID..." @@ -52,4 +46,5 @@ sleep 30 # Take site back online echo "Bringing site back online..." -rm $DOCUMENT_ROOT/maintenance.file +rm -f "$MAINT_FILE" +trap - EXIT INT TERM diff --git a/deploy-unstable.sh b/deploy-unstable.sh index 7ac97441..6731fad4 100644 --- a/deploy-unstable.sh +++ b/deploy-unstable.sh @@ -1,5 +1,13 @@ #!/bin/sh +set -eu + DOCUMENT_ROOT=/var/www/html +MAINT_FILE="$DOCUMENT_ROOT/maintenance.file" + +cleanup() { + rm -f "$MAINT_FILE" +} +trap cleanup EXIT INT TERM # Sign sources echo "Signing all sources..." @@ -11,27 +19,19 @@ echo "Building content..." # Take site offline echo "Taking site offline..." -touch $DOCUMENT_ROOT/maintenance.file +touch "$MAINT_FILE" # Swap over the content echo "Deploying content..." -cp ./app/build/outputs/apk/unstable/release/app-unstable-x86_64-release.apk $DOCUMENT_ROOT/app-x86_64-release-unstable.apk -cp ./app/build/outputs/apk/unstable/release/app-unstable-arm64-v8a-release.apk $DOCUMENT_ROOT/app-arm64-v8a-release-unstable.apk -cp ./app/build/outputs/apk/unstable/release/app-unstable-armeabi-v7a-release.apk $DOCUMENT_ROOT/app-armeabi-v7a-release-unstable.apk -cp ./app/build/outputs/apk/unstable/release/app-unstable-universal-release.apk $DOCUMENT_ROOT/app-universal-release-unstable.apk -cp ./app/build/outputs/apk/unstable/release/app-unstable-x86-release.apk $DOCUMENT_ROOT/app-x86-release-unstable.apk -cp ./app/build/outputs/apk/unstable/release/app-unstable-arm64-v8a-release.apk $DOCUMENT_ROOT/app-release-unstable.apk -git describe --tags > $DOCUMENT_ROOT/version-unstable.txt +cp ./app/build/outputs/apk/unstable/release/app-unstable-x86_64-release.apk "$DOCUMENT_ROOT/app-x86_64-release-unstable.apk" +cp ./app/build/outputs/apk/unstable/release/app-unstable-arm64-v8a-release.apk "$DOCUMENT_ROOT/app-arm64-v8a-release-unstable.apk" +cp ./app/build/outputs/apk/unstable/release/app-unstable-armeabi-v7a-release.apk "$DOCUMENT_ROOT/app-armeabi-v7a-release-unstable.apk" +cp ./app/build/outputs/apk/unstable/release/app-unstable-universal-release.apk "$DOCUMENT_ROOT/app-universal-release-unstable.apk" +cp ./app/build/outputs/apk/unstable/release/app-unstable-x86-release.apk "$DOCUMENT_ROOT/app-x86-release-unstable.apk" +cp ./app/build/outputs/apk/unstable/release/app-unstable-arm64-v8a-release.apk "$DOCUMENT_ROOT/app-release-unstable.apk" -aws s3 cp ./app/build/outputs/apk/unstable/release/app-unstable-x86_64-release.apk s3://artifacts-grayjay-app/app-x86_64-release-unstable.apk -aws s3 cp ./app/build/outputs/apk/unstable/release/app-unstable-arm64-v8a-release.apk s3://artifacts-grayjay-app/app-arm64-v8a-release-unstable.apk -aws s3 cp ./app/build/outputs/apk/unstable/release/app-unstable-armeabi-v7a-release.apk s3://artifacts-grayjay-app/app-armeabi-v7a-release-unstable.apk -aws s3 cp ./app/build/outputs/apk/unstable/release/app-unstable-universal-release.apk s3://artifacts-grayjay-app/app-universal-release-unstable.apk -aws s3 cp ./app/build/outputs/apk/unstable/release/app-unstable-x86-release.apk s3://artifacts-grayjay-app/app-x86-release-unstable.apk -aws s3 cp ./app/build/outputs/apk/unstable/release/app-unstable-arm64-v8a-release.apk s3://artifacts-grayjay-app/app-release-unstable.apk - -git describe --tags > ./version-unstable.txt -aws s3 cp ./version-unstable.txt s3://artifacts-grayjay-app/version-unstable.txt +VERSION="$(git describe --tags)" +echo "$VERSION" > "$DOCUMENT_ROOT/version-unstable.txt" # Notify Cloudflare to wipe the CDN cache echo "Purging Cloudflare cache for zone $CLOUDFLARE_ZONE_ID..." @@ -44,4 +44,5 @@ sleep 30 # Take site back online echo "Bringing site back online..." -rm $DOCUMENT_ROOT/maintenance.file +rm -f "$MAINT_FILE" +trap - EXIT INT TERM diff --git a/publish_playstore.py b/publish_playstore.py new file mode 100644 index 00000000..1eb1bf46 --- /dev/null +++ b/publish_playstore.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +import argparse +import os +import sys +import random +import time +import httplib2 +import socket + +from google_auth_httplib2 import AuthorizedHttp +from google.oauth2 import service_account +from googleapiclient.discovery import build +from googleapiclient.http import MediaFileUpload +from googleapiclient.errors import HttpError +from googleapiclient.http import build_http + +SCOPE = "https://www.googleapis.com/auth/androidpublisher" +socket.setdefaulttimeout(30 * 60) + +def die(msg: str, code: int = 1): + print(msg, file=sys.stderr) + raise SystemExit(code) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--sa", required=True, help="Service account JSON file path") + ap.add_argument("--package", required=True, help="ApplicationId / package name") + ap.add_argument("--aab", required=True, help="Path to .aab file") + ap.add_argument("--track", default="internal", help="internal|alpha|beta|production") + ap.add_argument("--status", default="completed", help="draft|inProgress|halted|completed") + ap.add_argument("--name", default=None, help="Release name (defaults to CI_COMMIT_TAG)") + ap.add_argument("--rollout", type=float, default=None, help="For staged rollout: 0 < rollout < 1") + args = ap.parse_args() + + if not os.path.isfile(args.sa): + die(f"Missing service account JSON: {args.sa}") + if not os.path.isfile(args.aab): + die(f"Missing AAB: {args.aab}") + + release_name = args.name or os.environ.get("CI_COMMIT_TAG") + if not release_name: + die("Missing release name: pass --name or set CI_COMMIT_TAG") + + staged = args.status in ("inProgress", "halted") + if staged: + if args.rollout is None: + die("--rollout is required when --status is inProgress or halted") + if not (0.0 < args.rollout < 1.0): + die("--rollout must satisfy 0 < rollout < 1") + else: + args.rollout = None + + print(f"Loading service account") + + creds = service_account.Credentials.from_service_account_file( + args.sa, scopes=[SCOPE] + ) + + print(f"Loaded service account") + + + print(f"Building service") + http = build_http() + authed_http = AuthorizedHttp(creds, http=http) + service = build("androidpublisher", "v3", http=authed_http, cache_discovery=False) + print(f"Built service") + + try: + print(f"Creating edit") + + edit = service.edits().insert(body={}, packageName=args.package).execute() + edit_id = edit["id"] + + UPLOAD_CHUNK_SIZE = 10 * 1024 * 1024 + MAX_RETRIES = 8 + + print(f"Media upload started") + + media = MediaFileUpload( + args.aab, + mimetype="application/octet-stream", + resumable=True, + chunksize=UPLOAD_CHUNK_SIZE, + ) + + request = service.edits().bundles().upload( + packageName=args.package, + editId=edit_id, + media_body=media, + ) + + response = None + last_pct = -1 + attempt = 0 + + while response is None: + try: + status, response = request.next_chunk(num_retries=3) + attempt = 0 # reset after any successful chunk + + if status: + pct = int(status.progress() * 100) + if pct != last_pct: + last_pct = pct + print(f"Upload progress: {pct}%", flush=True) + + except HttpError as e: + # Retry transient server-side errors with exponential backoff + code = getattr(getattr(e, "resp", None), "status", None) + if code in (500, 502, 503, 504) and attempt < MAX_RETRIES: + sleep_s = min(60, (2 ** attempt)) + random.random() + print(f"Transient HTTP {code}; retrying in {sleep_s:.1f}s...", flush=True) + time.sleep(sleep_s) + attempt += 1 + continue + raise + + print("Media upload finished") + bundle = response + version_code = bundle["versionCode"] + + release = { + "name": release_name, + "status": args.status, + "versionCodes": [str(version_code)], + } + if args.rollout is not None: + release["userFraction"] = args.rollout + + track_body = {"releases": [release]} + + print(f"Updating track") + + service.edits().tracks().update( + packageName=args.package, + editId=edit_id, + track=args.track, + body=track_body, + ).execute() + + print(f"Updated track") + print(f"Committing") + + service.edits().commit(packageName=args.package, editId=edit_id).execute() + print(f"Committed") + + print(f"OK: package={args.package} track={args.track} status={args.status} versionCode={version_code} name={release_name}") + except HttpError as e: + content = e.content.decode("utf-8", errors="replace") if getattr(e, "content", None) else str(e) + die(f"Google API error (HTTP {e.resp.status if e.resp else '??'}):\n{content}") + except Exception as e: + die(f"Unexpected error: {e}") + +if __name__ == "__main__": + main() diff --git a/venv-playstore.sh b/venv-playstore.sh new file mode 100644 index 00000000..c16ef814 --- /dev/null +++ b/venv-playstore.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +VENV_DIR="${VENV_DIR:-.venv-playstore}" + +if [[ ! -d "$VENV_DIR" ]]; then + python3 -m venv "$VENV_DIR" +fi + +source "$VENV_DIR/bin/activate" + +python -m pip install --upgrade pip setuptools wheel +python -m pip install --upgrade google-api-python-client google-auth google-auth-httplib2