diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79a2a52beb..5d73b62f86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -582,60 +582,16 @@ jobs: echo "FIREBASE_CONSOLE_URL=$FIREBASE_URL" >> $GITHUB_OUTPUT - - name: Set up SSH key + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Upload APK to Cloudflare R2 env: - GREENGEEKS_HOST: ${{ vars.GREENGEEKS_SSH_HOST }} - GREENGEEKS_KEY: ${{ secrets.GREENGEEKS_SSH_PRIVATE_KEY }} - GREENGEEKS_USER: ${{ vars.GREENGEEKS_SSH_USER }} + CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_KEY_ID: ${{ vars.CLOUDFLARE_KEY_ID }} + CLOUDFLARE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }} run: | - mkdir -p ~/.ssh - if [ -z "$GREENGEEKS_HOST" ]; then - echo "Error: SSH_HOST variable is not set" - exit 1 - fi - # Write the SSH key, ensuring proper formatting - echo "$GREENGEEKS_KEY" > ~/.ssh/id_rsa - # Remove any trailing newlines and ensure proper key format - sed -i '' -e '$ { /^$/ d; }' ~/.ssh/id_rsa 2>/dev/null || sed -i '$ { /^$/ d; }' ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - # Verify key format - if ! grep -q "BEGIN.*PRIVATE KEY" ~/.ssh/id_rsa; then - echo "Error: SSH key does not appear to be in correct format" - exit 1 - fi - # Configure SSH to use only the key file and disable other auth methods - cat > ~/.ssh/config </dev/null - ssh-keyscan -H "$GREENGEEKS_HOST" >> ~/.ssh/known_hosts 2>/dev/null + uv run --with boto3 scripts/cloudflare-r2-upload.py "${{ steps.find_apk.outputs.APK_PATH }}" "${{ matrix.variant }}" - name: Clean up build folder after upload run: | @@ -735,6 +691,27 @@ jobs: rm -f payload.json + - name: Send Telegram message + env: + TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + TELEGRAM_EARLY_ACCESS_CHAT_ID: ${{ vars.TELEGRAM_EARLY_ACCESS_CHAT_ID }} + APK_PATH: ${{ steps.find_apk.outputs.APK_PATH }} + VARIANT: ${{ matrix.variant }} + run: | + GIT_LOG=$(git log --oneline --since "24 hours ago" || true) + if [ -z "$GIT_LOG" ]; then + GIT_LOG="(no commits in the last 24 hours)" + fi + APK_BASENAME=$(basename "$APK_PATH") + APK_FILENAME="${APK_BASENAME%.*}-${VARIANT}.${APK_BASENAME##*.}" + DOWNLOAD_URL="https://download.appdevforall.org/${APK_FILENAME}" + MESSAGE=$( printf "Download: %s\n\n%s" "$DOWNLOAD_URL" "$GIT_LOG" ) + # Telegram message limit 4096; use first 4096 chars + MESSAGE="${MESSAGE:0:4096}" + curl -s -X POST -H "Content-Type: application/json" \ + -d "$(jq -n --arg chat_id "$TELEGRAM_EARLY_ACCESS_CHAT_ID" --arg text "$MESSAGE" '{chat_id: $chat_id, text: $text}')" \ + "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" + - name: Cleanup google-services.json if: always() run: | diff --git a/scripts/cloudflare-r2-upload.py b/scripts/cloudflare-r2-upload.py new file mode 100644 index 0000000000..be0631b1b7 --- /dev/null +++ b/scripts/cloudflare-r2-upload.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Upload a file (e.g. APK) to Cloudflare R2. Credentials via env: CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_KEY_ID, CLOUDFLARE_SECRET_ACCESS_KEY.""" +import sys +import os +import boto3 +from botocore.config import Config + +REQUIRED_ENV = ( + "CLOUDFLARE_ACCOUNT_ID", + "CLOUDFLARE_KEY_ID", + "CLOUDFLARE_SECRET_ACCESS_KEY", +) + +for name in REQUIRED_ENV: + if not os.environ.get(name): + print(f"ERROR: {name} environment variable is not set.", file=sys.stderr) + sys.exit(1) + +CLOUDFLARE_ACCOUNT_ID = os.environ["CLOUDFLARE_ACCOUNT_ID"] +CLOUDFLARE_KEY_ID = os.environ["CLOUDFLARE_KEY_ID"] +CLOUDFLARE_SECRET_ACCESS_KEY = os.environ["CLOUDFLARE_SECRET_ACCESS_KEY"] +BUCKET_NAME = "apk-repo" + +R2_ENDPOINT_URL = f"https://{CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com" + +config = Config( + read_timeout=300, + connect_timeout=60, + retries={"max_attempts": 10}, +) + +s3 = boto3.client( + service_name="s3", + endpoint_url=R2_ENDPOINT_URL, + aws_access_key_id=CLOUDFLARE_KEY_ID, + aws_secret_access_key=CLOUDFLARE_SECRET_ACCESS_KEY, + region_name="auto", + config=config, +) + +if len(sys.argv) < 3: + print("Usage: cloudflare-r2-upload.py ", file=sys.stderr) + sys.exit(1) + +file_path = sys.argv[1] +variant = sys.argv[2] +file_size = os.path.getsize(file_path) +base_name = os.path.basename(file_path) +# Inject variant into filename before .apk so v7 and v8 upload to distinct R2 keys +name_root, ext = os.path.splitext(base_name) +file_name = f"{name_root}-{variant}{ext}" + +extra_args = {} +if file_name.lower().endswith(".apk"): + extra_args["ContentType"] = "application/vnd.android.package-archive" + +# Progress callback: print new lines at 10% intervals for CI-friendly logs (bytes_amount is incremental per call) +_seen_so_far = [0] +_last_printed_pct = [-1] + +def progress_callback(bytes_amount): + _seen_so_far[0] += bytes_amount + if file_size <= 0: + return + pct = int(100 * _seen_so_far[0] / file_size) + if pct >= _last_printed_pct[0] + 10 or pct == 100: + _last_printed_pct[0] = pct + mb = _seen_so_far[0] / (1024 * 1024) + total_mb = file_size / (1024 * 1024) + print(f"Upload progress: {pct}% ({mb:.1f} MB / {total_mb:.1f} MB)", flush=True) + + +upload_kwargs = {"Callback": progress_callback} +if extra_args: + upload_kwargs["ExtraArgs"] = extra_args + +print(f"Uploading {file_name} ({file_size / (1024*1024):.1f} MB) to R2...", flush=True) +s3.upload_file(file_path, BUCKET_NAME, file_name, **upload_kwargs) +print("Upload complete.", flush=True)