Skip to content

Generate Assets Zips and Upload to Google Drive #93

Generate Assets Zips and Upload to Google Drive

Generate Assets Zips and Upload to Google Drive #93

Workflow file for this run

name: Generate Assets Zips
permissions:
id-token: write
contents: write
actions: write
on:
workflow_dispatch:
inputs:
ref:
description: Branch or tag to build the asset zips from
required: false
default: stage
asset_source:
description: Where the debug asset ingredients come from
required: false
default: site
type: choice
options:
- site
- candidate
- release
candidate_path:
description: 'GreenGeeks candidate staging path (asset_source: candidate), e.g. tmp/assets/candidates/42'
required: false
default: ''
asset_release_tag:
description: 'Release tag holding the debug ingredients (asset_source: release)'
required: false
default: adfa-2602-rc
env:
SCP_HOST: ${{ vars.GREENGEEKS_SSH_HOST }}
jobs:
prepare:
name: Stage debug assets
runs-on: self-hosted
timeout-minutes: 90
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- name: Remove stale staged assets
run: |
rm -f assets/*.zip assets/documentation.db assets/core.cgt
ls -la assets/ 2>/dev/null || true
- name: Check if Nix is installed
id: check_nix
run: |
if command -v nix >/dev/null 2>&1; then
echo "nix is installed"
echo "nix_installed=true" >> $GITHUB_ENV
else
echo "nix is not installed"
echo "nix_installed=false" >> $GITHUB_ENV
fi
- name: Install Flox
if: env.nix_installed == 'false'
uses: flox/install-flox-action@v2
- name: Create google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: |
echo "$GOOGLE_SERVICES_JSON" > app/google-services.json
echo "google-services.json created successfully"
- name: Authenticate to Google Cloud for Drive access
id: auth_drive
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.IDENTITY_EMAIL }}
token_format: 'access_token'
access_token_scopes: 'https://www.googleapis.com/auth/drive'
- name: Set up SSH key
if: inputs.asset_source == 'site' || inputs.asset_source == 'candidate'
env:
GREENGEEKS_HOST: ${{ vars.GREENGEEKS_SSH_HOST }}
GREENGEEKS_KEY: ${{ secrets.GREENGEEKS_SSH_PRIVATE_KEY }}
GREENGEEKS_USER: ${{ vars.GREENGEEKS_SSH_USER }}
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
- name: Download debug assets from the site
if: inputs.asset_source == 'site'
run: |
flox activate -d flox/base -- ./gradlew :app:assetsDownloadDebug --no-daemon \
-Dorg.gradle.jvmargs="-Xmx10g -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED" \
-Dandroid.aapt2.daemonHeapSize=4096M \
-Dorg.gradle.workers.max=1 \
-Dorg.gradle.parallel=false
- name: Download debug assets from the candidate staging area
if: inputs.asset_source == 'candidate'
env:
GREENGEEKS_HOST: ${{ vars.GREENGEEKS_SSH_HOST }}
CANDIDATE_PATH: ${{ inputs.candidate_path }}
run: |
set -euo pipefail
if [ -z "$CANDIDATE_PATH" ]; then
echo "ERROR: asset_source is 'candidate' but candidate_path is empty."
echo " Take the path from the dev-assets asset-set run summary."
exit 1
fi
mkdir -p assets
scp "$GREENGEEKS_HOST:$CANDIDATE_PATH/debug/*" assets/
rm -f assets/*.md5
ls -la assets/
- name: Download debug assets from a release
if: inputs.asset_source == 'release'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ASSET_TAG: ${{ inputs.asset_release_tag }}
run: |
mkdir -p assets
gh release download "$ASSET_TAG" --dir assets --clobber \
--pattern 'android-sdk-*.zip' \
--pattern 'bootstrap-*.zip' \
--pattern 'gradle-*-bin.zip' \
--pattern 'gradle-api-*.jar.zip' \
--pattern 'localMvnRepository.zip' \
--pattern 'core.cgt'
ls -la assets/
- name: Download latest documentation.db from Google Drive
run: |
DB_FILE_ID="${{ secrets.DOCUMENTATION_DB_FILE_ID }}"
ACCESS_TOKEN="${{ steps.auth_drive.outputs.access_token }}"
if [ -z "$DB_FILE_ID" ]; then
echo "ERROR: DOCUMENTATION_DB_FILE_ID secret not set"
echo "Please set the DOCUMENTATION_DB_FILE_ID secret in repository settings"
exit 1
fi
echo "Downloading documentation.db from Google Drive..."
mkdir -p assets
curl -sL -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://www.googleapis.com/drive/v3/files/${DB_FILE_ID}?alt=media&supportsAllDrives=true&acknowledgeAbuse=true" \
-o assets/documentation.db
if [ ! -f assets/documentation.db ]; then
echo "ERROR: Failed to download documentation.db"
exit 1
fi
FILE_SIZE_BYTES=$(stat -c%s assets/documentation.db 2>/dev/null || stat -f%z assets/documentation.db 2>/dev/null)
FILE_SIZE_HUMAN=$(du -h assets/documentation.db | cut -f1)
if [ "$FILE_SIZE_BYTES" -lt 1000000 ]; then
echo "ERROR: Downloaded file is too small ($FILE_SIZE_HUMAN)"
echo "This usually means the file was not found or service account lacks access"
exit 1
fi
echo "Successfully downloaded documentation.db ($FILE_SIZE_HUMAN)"
- name: Verify staged ingredients
run: |
missing=0
for f in localMvnRepository.zip documentation.db core.cgt \
android-sdk-arm64-v8a.zip android-sdk-armeabi-v7a.zip \
bootstrap-arm64-v8a.zip bootstrap-armeabi-v7a.zip; do
if [ ! -s "assets/$f" ]; then
echo "ERROR: missing or empty assets/$f"
missing=1
fi
done
if ! ls assets/gradle-*-bin.zip >/dev/null 2>&1; then
echo "ERROR: no assets/gradle-*-bin.zip staged"
missing=1
fi
if ! ls assets/gradle-api-*.jar.zip >/dev/null 2>&1; then
echo "ERROR: no assets/gradle-api-*.jar.zip staged"
missing=1
fi
[ "$missing" -eq 0 ] || exit 1
command -v sqlite3 >/dev/null || { sudo apt-get update -qq && sudo apt-get install -y -qq sqlite3; }
sqlite3 assets/documentation.db \
"SELECT 1 FROM pragma_table_info('Content') WHERE name='templateId';" | grep -q 1 \
|| { echo "ERROR: documentation.db predates the templateId column; docs requests will 500"; exit 1; }
echo "All ingredients staged"
- name: Upload staged assets
uses: actions/upload-artifact@v4
with:
name: debug-assets
path: assets/
retention-days: 1
if-no-files-found: error
- name: Cleanup google-services.json
if: always()
run: |
rm -f app/google-services.json
echo "google-services.json cleaned up successfully"
- name: Cleanup ssh
if: always() && (inputs.asset_source == 'site' || inputs.asset_source == 'candidate')
run: |
# Remove SSH key
rm -f ~/.ssh/id_rsa
# Clean up SSH known_hosts (remove the entry for this host)
if [ -n "$SCP_HOST" ]; then
ssh-keygen -R "$SCP_HOST" 2>/dev/null || true
fi
# Remove entire .ssh directory if empty
rmdir ~/.ssh 2>/dev/null || true
zip:
name: Zip assets
runs-on: self-hosted
timeout-minutes: 60
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- abi: v8
arch: arm64-v8a
label: 64-bit ARM
- abi: v7
arch: armeabi-v7a
label: 32-bit ARM
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- name: Remove stale staged assets
run: |
rm -f assets/*.zip assets/documentation.db assets/core.cgt
- name: Check if Nix is installed
id: check_nix
run: |
if command -v nix >/dev/null 2>&1; then
echo "nix is installed"
echo "nix_installed=true" >> $GITHUB_ENV
else
echo "nix is not installed"
echo "nix_installed=false" >> $GITHUB_ENV
fi
- name: Install Flox
if: env.nix_installed == 'false'
uses: flox/install-flox-action@v2
- name: Create google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: |
echo "$GOOGLE_SERVICES_JSON" > app/google-services.json
- name: Download staged assets
uses: actions/download-artifact@v4
with:
name: debug-assets
path: assets
- name: Assemble assets zip
run: |
variant_upper=$(echo "${{ matrix.abi }}" | tr '[:lower:]' '[:upper:]')
flox activate -d flox/base -- ./gradlew :app:assemble${variant_upper}Assets --no-daemon \
-Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED" \
-Dandroid.aapt2.daemonHeapSize=4096M \
-Dorg.gradle.workers.max=1 \
-Dorg.gradle.parallel=false
- name: Verify assets zip
id: assets_zip
run: |
zip_path="app/build/outputs/assets/assets-${{ matrix.arch }}.zip"
if [ ! -s "$zip_path" ]; then
echo "ERROR: $zip_path was not produced"
exit 1
fi
unzip -tq "$zip_path"
for entry in android-sdk.zip bootstrap.zip localMvnRepository.zip documentation.db \
core.cgt plugin-artifacts.zip plugin-maven-repo.zip; do
unzip -l "$zip_path" | grep -qE "[[:space:]]$entry$" \
|| { echo "ERROR: $zip_path is missing entry $entry"; exit 1; }
done
echo "ASSETS_PATH=$zip_path" >> $GITHUB_OUTPUT
echo "$(du -h "$zip_path" | cut -f1) $zip_path"
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Upload assets zip to Cloudflare R2
id: r2
env:
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_KEY_ID: ${{ vars.CLOUDFLARE_KEY_ID }}
CLOUDFLARE_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }}
R2_BUCKET: ${{ vars.R2_ASSETS_BUCKET || 'apk-repo' }}
R2_KEY_PREFIX: assets/
run: |
uv run --with boto3 scripts/cloudflare-r2-upload.py "${{ steps.assets_zip.outputs.ASSETS_PATH }}"
echo "DOWNLOAD_URL=https://download.appdevforall.org/assets/assets-${{ matrix.arch }}.zip" >> $GITHUB_OUTPUT
- name: Send Slack notification
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
ASSET_LABEL: ${{ matrix.label }}
DOWNLOAD_URL: ${{ steps.r2.outputs.DOWNLOAD_URL }}
BUILD_REF: ${{ inputs.ref }}
run: |
jq -n \
--arg label "$ASSET_LABEL" \
--arg url "$DOWNLOAD_URL" \
--arg ref "$BUILD_REF" \
'{
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: ":rocket: [Updated] New Assets Zip Available",
emoji: true
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: "*\($label):* <\($url)|Download assets zip>"
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: "Built from `\($ref)`. Push to `/sdcard/Download/` on the device, then `adb shell touch` it."
}
]
}
]
}' > payload.json
curl -X POST -H "Content-type: application/json" --data @payload.json "$SLACK_WEBHOOK"
rm -f payload.json
- name: Clean up outputs
if: always()
run: |
rm -f app/google-services.json
rm -rf app/build/outputs/assets/