Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 29 additions & 52 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
Host *
IdentitiesOnly yes
PreferredAuthentications publickey
StrictHostKeyChecking no
UserKnownHostsFile ~/.ssh/known_hosts
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
GSSAPIAuthentication no
GSSAPIKeyExchange no
GSSAPIDelegateCredentials no
Host $GREENGEEKS_HOST
User $GREENGEEKS_USER
IdentityFile ~/.ssh/id_rsa
IdentitiesOnly yes
PreferredAuthentications publickey
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
GSSAPIAuthentication no
GSSAPIKeyExchange no
GSSAPIDelegateCredentials no
NumberOfPasswordPrompts 0
EOF
chmod 600 ~/.ssh/config
# Disable SSH agent completely
unset SSH_AUTH_SOCK
unset SSH_AGENT_PID
# Remove any default SSH keys that might interfere
rm -f ~/.ssh/id_ed25519 ~/.ssh/id_ecdsa ~/.ssh/id_dsa ~/.ssh/id_rsa.pub 2>/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: |
Expand Down Expand Up @@ -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: |
Expand Down
79 changes: 79 additions & 0 deletions scripts/cloudflare-r2-upload.py
Original file line number Diff line number Diff line change
@@ -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_path> <variant>", 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)
Loading