From 8507699cdd2c50cb5c8dbe7299d9482ebc0d8fcb Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 1 Jun 2026 10:24:52 +0200 Subject: [PATCH] feat(eas): force bun on EAS via custom build configs + 5-build release workflow (#1632) --- .eas/build/android-production-apk.yml | 25 +++++ .eas/build/android-production-tv.yml | 27 ++++++ .eas/build/android-production.yml | 38 ++++++++ .eas/build/ios-production.yml | 44 +++++++++ .github/workflows/release.yml | 133 +++++++++++++++++++++----- .gitignore | 3 + eas.json | 15 ++- 7 files changed, 256 insertions(+), 29 deletions(-) create mode 100644 .eas/build/android-production-apk.yml create mode 100644 .eas/build/android-production-tv.yml create mode 100644 .eas/build/android-production.yml create mode 100644 .eas/build/ios-production.yml diff --git a/.eas/build/android-production-apk.yml b/.eas/build/android-production-apk.yml new file mode 100644 index 000000000..757192bb6 --- /dev/null +++ b/.eas/build/android-production-apk.yml @@ -0,0 +1,25 @@ +# Custom EAS Build config for Android phone APK (downloadable artifact). +# Same bun-forcing flow as android-production.yml, but builds an APK +# (assembleRelease) instead of an AAB — for sideloading / GitHub artifact. +# Referenced from eas.json: build.production-apk.android.config +build: + name: Android phone APK (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + - run: + name: Prebuild (Android, bun) + command: bunx expo prebuild --platform android --no-install + + - eas/configure_android_version + - eas/inject_android_credentials + + - eas/run_gradle: + inputs: + command: :app:assembleRelease + + - eas/find_and_upload_build_artifacts diff --git a/.eas/build/android-production-tv.yml b/.eas/build/android-production-tv.yml new file mode 100644 index 000000000..a3fc9effe --- /dev/null +++ b/.eas/build/android-production-tv.yml @@ -0,0 +1,27 @@ +# Custom EAS Build config for Android TV APK (downloadable artifact). +# Same bun-forcing flow, with EXPO_TV=1 (set via the profile env in +# eas.json) so prebuild generates the TV variant. Builds an APK for +# sideloading onto Android TV devices. +# Referenced from eas.json: build.production-apk-tv.android.config +build: + name: Android TV APK (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + # EXPO_TV=1 comes from the profile env, so prebuild targets Android TV. + - run: + name: Prebuild (Android TV, bun) + command: bunx expo prebuild --platform android --no-install + + - eas/configure_android_version + - eas/inject_android_credentials + + - eas/run_gradle: + inputs: + command: :app:assembleRelease + + - eas/find_and_upload_build_artifacts diff --git a/.eas/build/android-production.yml b/.eas/build/android-production.yml new file mode 100644 index 000000000..651ff2b66 --- /dev/null +++ b/.eas/build/android-production.yml @@ -0,0 +1,38 @@ +# Custom EAS Build config for Android (production AAB). +# +# Why this exists: EAS's managed build can't detect Bun's text lockfile +# (bun.lock) and falls back to yarn, which breaks our install. The managed +# steps `eas/install_node_modules` and `eas/prebuild` both use "the package +# manager detected based on your project", so we replace them with explicit +# `bun` commands. Everything else uses EAS's built-in functions so we still +# get remote versioning, credentials, and artifact upload. +# +# Referenced from eas.json: build.production.android.config = android-production.yml +build: + name: Android production (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + # android/ is gitignored, so generate native code fresh. --no-install + # because deps are already installed above; bunx keeps it on bun. + - run: + name: Prebuild (Android, bun) + command: bunx expo prebuild --platform android --no-install + + # Applies the EAS-resolved remote versionCode/versionName (autoIncrement + # in eas.json) into the freshly prebuilt android/ project. + - eas/configure_android_version + + # Injects the remote Android keystore / signing config. + - eas/inject_android_credentials + + # Build the Play Store app bundle (.aab). + - eas/run_gradle: + inputs: + command: :app:bundleRelease + + - eas/find_and_upload_build_artifacts diff --git a/.eas/build/ios-production.yml b/.eas/build/ios-production.yml new file mode 100644 index 000000000..f9e228f72 --- /dev/null +++ b/.eas/build/ios-production.yml @@ -0,0 +1,44 @@ +# Custom EAS Build config for iOS + tvOS (App Store), forcing bun. +# +# Shared by both the iPhone profile (production) and the tvOS profile +# (production_tv). The profile decides the rest: +# - production_tv sets EXPO_TV=1 (env) so prebuild targets tvOS, and +# credentialsSource: local (EAS can't manage tvOS creds remotely). +# - production uses remote-managed iOS credentials. +# +# Like the Android configs, this replaces eas/install_node_modules and +# eas/prebuild (both auto-detect the wrong package manager) with explicit +# bun commands, and keeps EAS built-ins for credentials/version/fastlane. +build: + name: iOS/tvOS App Store (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + - eas/resolve_apple_team_id_from_credentials: + id: resolve_team + + # android/ + ios/ are gitignored, so generate native code fresh. + # EXPO_TV (from the profile env) selects iPhone vs tvOS. --no-install + # skips JS + pod install; we install pods explicitly below with bun deps. + - run: + name: Prebuild (iOS/tvOS, bun) + command: bunx expo prebuild --platform ios --no-install + + - run: + name: Install CocoaPods + working_directory: ./ios + command: pod install + + - eas/configure_ios_credentials + - eas/configure_ios_version + + - eas/generate_gymfile_from_template: + inputs: + credentials: ${ eas.job.secrets.buildCredentials } + + - eas/run_fastlane + - eas/find_and_upload_build_artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e9da617f..28effbd89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,14 @@ -name: 🚀 Release (EAS Build + Submit) +name: 🚀 Release (EAS build + submit) -# Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main. -# A manual approval gate (the `production` GitHub Environment) pauses the run -# before any build/submit starts. Configure required reviewers on that -# environment in repo Settings → Environments → production. +# On merge to main (gated by the `production` GitHub Environment approval), +# build all targets on EAS in parallel via custom bun build configs: +# 1. iOS phone → App Store (auto-submit) +# 2. tvOS → App Store (auto-submit) +# 3. Android AAB → Google Play (auto-submit) +# 4. Android phone APK→ downloadable artifact +# 5. Android TV APK → downloadable artifact +# Note: EAS queues builds based on your plan's concurrency; parallel jobs +# here just submit them — EAS may still run them serially. concurrency: group: release-${{ github.ref }} @@ -23,7 +28,7 @@ jobs: - name: ✅ Release approved run: echo "Release approved for ${{ github.sha }}" - release: + build: name: 🚀 ${{ matrix.name }} needs: approve runs-on: ubuntu-24.04 @@ -36,12 +41,25 @@ jobs: - name: 🍎 iOS platform: ios profile: production + submit: true - name: 📺 tvOS platform: ios profile: production_tv - - name: 🤖 Android + submit: true + - name: 🤖 Android AAB platform: android profile: production + submit: true + - name: 🤖 Android APK + platform: android + profile: production-apk + submit: false + artifact_name: streamyfin-android-phone-apk + - name: 📺 Android TV APK + platform: android + profile: production-apk-tv + submit: false + artifact_name: streamyfin-android-tv-apk steps: - name: 📥 Checkout code @@ -76,10 +94,8 @@ jobs: token: ${{ secrets.EXPO_TOKEN }} eas-cache: true - # tvOS uses local credentials (EAS can't manage tvOS provisioning - # remotely, including the TopShelf extension target). Restore the - # gitignored credentials.json + cert + profiles from secrets so the - # cloud build can sign with `credentialsSource: local`. + # tvOS uses credentialsSource: local — restore the gitignored + # credentials.json + cert + provisioning profiles from secrets. - name: 🔐 Restore tvOS signing credentials if: matrix.profile == 'production_tv' env: @@ -94,10 +110,14 @@ jobs: echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision - # iOS + tvOS submit upload to App Store Connect with an ASC API key. - # EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID / - # EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8, - # tolerating either raw-PEM or base64-encoded secret content. + # Android Play submit needs the Google Play service account JSON. + - name: 🔐 Restore Google Play service account + if: matrix.platform == 'android' && matrix.submit + env: + GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json + + # App Store Connect API key for iOS/tvOS submit (raw-PEM or base64). - name: 🔐 Restore App Store Connect API key if: matrix.platform == 'ios' env: @@ -109,18 +129,11 @@ jobs: printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8" fi - # Android submit needs a Google Play service account JSON. eas.json's - # submit.production.android.serviceAccountKeyPath points at this file. - - name: 🔐 Restore Google Play service account - if: matrix.platform == 'android' - env: - GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} - run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json - + # ── Submit builds: cloud build + auto-submit to the store ── - name: 🚀 Build & submit (${{ matrix.name }}) + if: matrix.submit env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - # Consumed by eas submit for iOS/tvOS; ignored for Android. EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8 EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }} EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }} @@ -129,4 +142,74 @@ jobs: --platform ${{ matrix.platform }} \ --profile ${{ matrix.profile }} \ --auto-submit \ - --non-interactive + --non-interactive \ + --wait + + # ── Artifact builds: cloud build, then download + upload the APK ── + - name: 🏗️ Build artifact (${{ matrix.name }}) + if: ${{ !matrix.submit }} + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + run: | + eas build \ + --platform ${{ matrix.platform }} \ + --profile ${{ matrix.profile }} \ + --non-interactive \ + --wait \ + --json > build-result.json + URL=$(node -e "const b=require('./build-result.json'); const x=Array.isArray(b)?b[0]:b; console.log(x.artifacts.applicationArchiveUrl)") + echo "Downloading artifact: $URL" + curl -fL "$URL" -o "${{ matrix.artifact_name }}.apk" + + - name: 📤 Upload APK artifact (${{ matrix.name }}) + if: ${{ !matrix.submit }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.artifact_name }} + path: ${{ matrix.artifact_name }}.apk + retention-days: 14 + + # Draft a GitHub Release with the two APKs attached. The tag comes from the + # merged-in app version (app.json → expo.version), NOT the auto-incremented + # build number — so cutting a release is a deliberate version bump via PR. + github-release: + name: 📦 Draft GitHub Release + needs: build + if: ${{ !cancelled() }} + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: 📥 Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + show-progress: false + + - name: 📦 Download APK artifacts from this run + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p apks + gh run download ${{ github.run_id }} --name streamyfin-android-phone-apk --dir apks + gh run download ${{ github.run_id }} --name streamyfin-android-tv-apk --dir apks + ls -la apks + + - name: 📝 Draft release (tag = app.json version, not auto-bumped) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION=$(node -e "console.log(require('./app.json').expo.version)") + TAG="v$VERSION" + echo "Release tag from merged app version: $TAG" + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG exists — updating APK assets" + gh release upload "$TAG" apks/*.apk --clobber + else + echo "Creating draft release $TAG" + gh release create "$TAG" \ + --draft \ + --generate-notes \ + --title "$TAG" \ + apks/*.apk + fi diff --git a/.gitignore b/.gitignore index 46328035d..ef41769f9 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,9 @@ modules/background-downloader/android/build/* # ios:unsigned-build Artifacts build/ +# but keep EAS custom build configs (the generic build/ rule above matches .eas/build/) +!.eas/build/ +!.eas/build/** .claude/ .agents/skills/** skills-lock.json diff --git a/eas.json b/eas.json index c4c2b0e07..966881ce9 100644 --- a/eas.json +++ b/eas.json @@ -56,7 +56,11 @@ "environment": "production", "autoIncrement": true, "android": { - "image": "latest" + "image": "latest", + "config": "android-production.yml" + }, + "ios": { + "config": "ios-production.yml" } }, "production-apk": { @@ -65,7 +69,8 @@ "autoIncrement": true, "android": { "buildType": "apk", - "image": "latest" + "image": "latest", + "config": "android-production-apk.yml" } }, "production-apk-tv": { @@ -74,7 +79,8 @@ "autoIncrement": true, "android": { "buildType": "apk", - "image": "latest" + "image": "latest", + "config": "android-production-tv.yml" }, "env": { "EXPO_TV": "1" @@ -88,7 +94,8 @@ "EXPO_TV": "1" }, "ios": { - "credentialsSource": "local" + "credentialsSource": "local", + "config": "ios-production.yml" } } },