Repository navigation
FIx [r2] #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Dev Build Android APK | |
| # Lightweight dev build: builds debug + release APKs, uploads them as | |
| # GitHub Actions artifacts, and optionally mirrors release APKs to Cloudflare | |
| # R2 when the commit message contains [r2]. | |
| # | |
| # Unlike build-android.yml (triggered on `build` branch), this workflow: | |
| # - triggers on `main` branch pushes + manual dispatch | |
| # - NEVER publishes a GitHub Release | |
| # - ALWAYS uploads to artifacts (not gated on [debug]) | |
| # - optionally uploads to R2 when [r2] is in the commit message | |
| # | |
| # The R2 upload is a separate job so it can use the `build` environment | |
| # (where R2 secrets live) without gating the build step behind environment | |
| # approvals. | |
| on: | |
| push: | |
| branches: [ dev ] | |
| workflow_dispatch: | |
| # GitHub Actions already skips workflows when the commit message contains | |
| # [skip ci], [ci skip], [no ci], [skip actions], or [actions skip]. | |
| # The job-level `if` below is an explicit, belt-and-suspenders guard. | |
| jobs: | |
| build: | |
| name: Build electerm Android APK (dev) | |
| runs-on: ubuntu-latest | |
| if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[no ci]') && !contains(github.event.head_commit.message, '[skip actions]') && !contains(github.event.head_commit.message, '[actions skip]') }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| - name: Set up Java (JDK 21) | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: 21 | |
| - name: Set up Android SDK | |
| uses: android-actions/setup-android@v3 | |
| - name: Accept Android SDK licenses | |
| run: yes | sdkmanager --licenses || true | |
| - name: Install required Android SDK packages | |
| run: sdkmanager "platforms;android-36" "build-tools;36.0.0" | |
| - name: ci init | |
| run: npm config set legacy-peer-deps true | |
| - name: Install root dependencies (electerm + build tooling) | |
| run: npm i | |
| - name: Install Capacitor dependencies | |
| run: npm --prefix build/android install | |
| - name: Build web frontend + Node.js backend bundle | |
| env: | |
| SERVER_SECRET: ${{ secrets.SERVER_SECRET }} | |
| run: npm run build:android | |
| - name: Add Android platform (first run only) | |
| run: cd build/android && [ -d android ] || npx cap add android | |
| - name: Sync Capacitor (copies web assets + native plugins) | |
| run: cd build/android && npx cap sync android | |
| - name: Ensure nodejs-mobile gradle variable is defined | |
| run: | | |
| cd build/android/android | |
| grep -q "nodejsMobileVersion" variables.gradle || \ | |
| printf '\next.nodejsMobileVersion = "18.20.4-capawesome.1"\n' >> variables.gradle | |
| - name: Apply icon / splash / MainActivity overlay | |
| run: | | |
| # AndroidManifest.xml must go at app/src/main/, NOT inside res/. | |
| MANIFEST_SRC=build/android/res-overlay/AndroidManifest.xml | |
| MANIFEST_DST=build/android/android/app/src/main/AndroidManifest.xml | |
| if [ -f "$MANIFEST_SRC" ]; then | |
| cp "$MANIFEST_SRC" "$MANIFEST_DST" | |
| echo "Overlayed AndroidManifest.xml -> $MANIFEST_DST" | |
| fi | |
| # Custom MainActivity.java (edge-to-edge fix for Android 15+). | |
| JAVA_SRC=build/android/res-overlay/java | |
| JAVA_DST=build/android/android/app/src/main/java | |
| if [ -d "$JAVA_SRC" ]; then | |
| mkdir -p "$JAVA_DST" | |
| cp -r "$JAVA_SRC/." "$JAVA_DST/" | |
| echo "Overlayed MainActivity.java -> $JAVA_DST" | |
| fi | |
| # Copy all resource dirs (drawable, mipmap-*, values, xml) into res/. | |
| cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ | |
| # Remove the stray res/java/ created by the bulk cp above. | |
| rm -rf build/android/android/app/src/main/res/java | |
| # Remove Capacitor's default resources that conflict with the overlay. | |
| RES=build/android/android/app/src/main/res | |
| rm -f "$RES/drawable-v24/ic_launcher_foreground.xml" | |
| rm -f "$RES/values/ic_launcher_background.xml" | |
| find "$RES" -type f -name 'splash.png' -path '*/drawable*' -delete | |
| - name: Configure per-ABI APK splits | |
| run: | | |
| GRADLE=build/android/android/app/build.gradle | |
| grep -q 'splits' "$GRADLE" || printf '%s\n' \ | |
| '' \ | |
| '// electerm: split per ABI (injected by CI)' \ | |
| 'android {' \ | |
| ' splits {' \ | |
| ' abi {' \ | |
| ' enable true' \ | |
| ' reset()' \ | |
| " include 'arm64-v8a', 'armeabi-v7a', 'x86_64'" \ | |
| ' universalApk true' \ | |
| ' }' \ | |
| ' }' \ | |
| '}' >> "$GRADLE" | |
| tail -n 15 "$GRADLE" | |
| - name: Build debug APK | |
| run: cd build/android/android && chmod +x gradlew && ./gradlew assembleDebug | |
| - name: Build release APK (signed with persistent keystore) | |
| env: | |
| ANDROID_RELEASE_KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }} | |
| ANDROID_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }} | |
| ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} | |
| ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} | |
| run: | | |
| cd build/android/android | |
| KEYSTORE="$PWD/app/release.keystore" | |
| printf '%s' "$ANDROID_RELEASE_KEYSTORE_BASE64" | base64 --decode > "$KEYSTORE" | |
| keytool -list -keystore "$KEYSTORE" -storepass "$ANDROID_RELEASE_KEYSTORE_PASSWORD" | |
| ./gradlew assembleRelease \ | |
| -Pandroid.injected.signing.store.file="$KEYSTORE" \ | |
| -Pandroid.injected.signing.store.password="$ANDROID_RELEASE_KEYSTORE_PASSWORD" \ | |
| -Pandroid.injected.signing.key.alias="$ANDROID_RELEASE_KEY_ALIAS" \ | |
| -Pandroid.injected.signing.key.password="$ANDROID_RELEASE_KEY_PASSWORD" | |
| - name: Rename APKs to electerm-android-{arch}-{version}.apk | |
| id: rename | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| APKROOT=build/android/android/app/build/outputs/apk | |
| mkdir -p "$APKROOT/named" | |
| for variant in debug release; do | |
| dir="$APKROOT/$variant" | |
| [ -d "$dir" ] || continue | |
| for apk in "$dir"/*.apk; do | |
| [ -f "$apk" ] || continue | |
| name=$(basename "$apk") | |
| arch=$(printf '%s' "$name" | grep -oE 'arm64-v8a|armeabi-v7a|x86_64|universal' | head -1) | |
| [ -z "$arch" ] && arch=universal | |
| if [ "$variant" = release ]; then | |
| out="electerm-android-$arch-$VERSION.apk" | |
| else | |
| out="electerm-android-$arch-$VERSION-debug.apk" | |
| fi | |
| cp "$apk" "$APKROOT/named/$out" | |
| echo " $variant/$name -> named/$out" | |
| done | |
| done | |
| echo "=== named APKs ===" | |
| ls -la "$APKROOT/named/" | |
| - name: Upload APKs (artifact) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: electerm-android-dev-apks | |
| path: build/android/android/app/build/outputs/apk/named/*.apk | |
| if-no-files-found: error | |
| retention-days: 30 | |
| # ----------------------------------------------------------------------- | |
| # R2 upload — only runs when [r2] is in the commit message. | |
| # Uses the `build` environment so R2 secrets (CF_R2_*) are available. | |
| # Downloads the APKs from the build job's artifact, then uploads release | |
| # APKs (skips debug) to the R2 bucket's temp/ prefix. | |
| # ----------------------------------------------------------------------- | |
| upload-to-r2: | |
| name: Upload release APKs to R2 | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: ${{ contains(github.event.head_commit.message, '[r2]') }} | |
| environment: build | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Download APK artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: electerm-android-dev-apks | |
| path: ./apks | |
| - name: List downloaded APKs | |
| run: ls -la ./apks | |
| - name: Configure AWS credentials for R2 | |
| run: | | |
| mkdir -p ~/.aws | |
| cat > ~/.aws/credentials << 'EOF' | |
| [default] | |
| aws_access_key_id = ${{ secrets.CF_R2_ACCESS_KEY_ID }} | |
| aws_secret_access_key = ${{ secrets.CF_R2_SECRET_ACCESS_KEY }} | |
| EOF | |
| cat > ~/.aws/config << 'EOF' | |
| [default] | |
| region = auto | |
| endpoint_url = ${{ secrets.CF_R2_ENDPOINT }} | |
| EOF | |
| - name: Upload release APKs to R2 (temp folder) | |
| env: | |
| CF_R2_BUCKET: ${{ secrets.CF_R2_BUCKET }} | |
| run: | | |
| for file in ./apks/*.apk; do | |
| [ -f "$file" ] || continue | |
| rel_path=$(basename "$file") | |
| case "$rel_path" in | |
| *-debug.apk) | |
| echo "Skipping debug APK for R2 upload: $rel_path" | |
| continue | |
| ;; | |
| esac | |
| aws s3 cp "$file" "s3://${CF_R2_BUCKET}/temp/${rel_path}" \ | |
| --acl public-read \ | |
| --cache-control "max-age=31536000" | |
| echo "Uploaded: temp/${rel_path}" | |
| done | |
| - name: Verify uploads | |
| env: | |
| CF_R2_BUCKET: ${{ secrets.CF_R2_BUCKET }} | |
| run: | | |
| aws s3 ls "s3://${CF_R2_BUCKET}/temp/" --recursive |