From ee4c4b75ad8fc9966772ef7dea6d10ad3ac1d7ed Mon Sep 17 00:00:00 2001 From: Uruk Date: Sat, 31 Jan 2026 18:10:45 +0100 Subject: [PATCH] feat: Enables iOS TV builds and unsigned builds Enables building of iOS TV apps and adds support for unsigned iOS builds. This change also enhances the artifact comment workflow by including file size and build duration in the download link. It also fixes the Crowdin integration by using a GitHub App token for authentication. --- .github/workflows/artifact-comment.yml | 39 ++++-- .github/workflows/build-apps.yml | 184 ++++++++++++++++--------- .github/workflows/crowdin.yml | 9 +- package.json | 1 + 4 files changed, 156 insertions(+), 77 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 8528a95b..288a916b 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -220,7 +220,10 @@ jobs: const jobMappings = { 'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'], 'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'], - 'iOS Phone': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone'] + 'iOS': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone'], + 'iOS Unsigned': ['🍎 Build iOS IPA (Phone - Unsigned)', 'build-ios-phone-unsigned'], + 'tvOS': ['🍎 Build tvOS IPA', 'build-ios-tv'], + 'tvOS Unsigned': ['🍎 Build tvOS IPA (Unsigned)', 'build-ios-tv-unsigned'] }; // Create individual status for each job @@ -353,10 +356,12 @@ jobs: // Process each expected build target individually const buildTargets = [ - { name: 'Android Phone', platform: '🤖', device: '📱', statusKey: 'Android Phone', artifactPattern: /android.*phone/i }, - { name: 'Android TV', platform: '🤖', device: '📺', statusKey: 'Android TV', artifactPattern: /android.*tv/i }, - { name: 'iOS Phone', platform: '🍎', device: '📱', statusKey: 'iOS Phone', artifactPattern: /ios.*phone/i }, - { name: 'iOS TV', platform: '🍎', device: '📺', statusKey: 'iOS TV', artifactPattern: /ios.*tv/i } + { name: 'Android Phone', platform: '🤖', device: '📱 Phone', statusKey: 'Android Phone', artifactPattern: /android.*phone/i }, + { name: 'Android TV', platform: '🤖', device: '📺 TV', statusKey: 'Android TV', artifactPattern: /android.*tv/i }, + { name: 'iOS', platform: '🍎', device: '📱 Phone', statusKey: 'iOS Phone', artifactPattern: /ios.*phone.*ipa(?!.*unsigned)/i }, + { name: 'iOS Unsigned', platform: '🍎', device: '📱 Phone Unsigned', statusKey: 'iOS Phone Unsigned', artifactPattern: /ios.*phone.*unsigned/i }, + { name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /ios.*tv.*ipa(?!.*unsigned)/i }, + { name: 'tvOS Unsigned', platform: '🍎', device: '📺 TV Unsigned', statusKey: 'tvOS Unsigned', artifactPattern: /ios.*tv.*unsigned/i } ]; for (const target of buildTargets) { @@ -371,16 +376,26 @@ jobs: let status = '⏳ Pending'; let downloadLink = '*Waiting for build...*'; - // Special case for iOS TV - show as disabled - if (target.name === 'iOS TV') { - status = '💤 Disabled'; - downloadLink = '*Disabled for now*'; - } else if (matchingStatus) { + if (matchingStatus) { if (matchingStatus.conclusion === 'success' && matchingArtifact) { status = '✅ Complete'; const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`; const fileType = target.name.includes('Android') ? 'APK' : 'IPA'; - downloadLink = `[📥 Download ${fileType}](${directLink})`; + + // Format file size + const sizeInMB = (matchingArtifact.size_in_bytes / (1024 * 1024)).toFixed(1); + const sizeInfo = `(${sizeInMB} MB)`; + + // Calculate build duration + let durationInfo = ''; + if (matchingStatus.started_at && matchingStatus.completed_at) { + const durationMs = new Date(matchingStatus.completed_at) - new Date(matchingStatus.started_at); + const durationMin = Math.floor(durationMs / 60000); + const durationSec = Math.floor((durationMs % 60000) / 1000); + durationInfo = ` - ${durationMin}m ${durationSec}s`; + } + + downloadLink = `[📥 Download ${fileType}](${directLink}) ${sizeInfo}${durationInfo}`; } else if (matchingStatus.conclusion === 'failure') { status = `❌ [Failed](${matchingStatus.url})`; downloadLink = '*Build failed*'; @@ -408,7 +423,7 @@ jobs: } } - commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`; + commentBody += `| ${target.platform} ${target.name} | ${target.device} | ${status} | ${downloadLink} |\n`; } commentBody += `\n`; diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index b300c04d..520cbb46 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -299,67 +299,123 @@ jobs: path: build/*.ipa retention-days: 7 - # Disabled for now - uncomment when ready to build iOS TV - # build-ios-tv: - # if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) - # runs-on: macos-26 - # name: 🍎 Build iOS IPA (TV) - # permissions: - # contents: read - # - # steps: - # - name: 📥 Checkout code - # uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - # with: - # ref: ${{ github.event.pull_request.head.sha || github.sha }} - # fetch-depth: 0 - # submodules: recursive - # show-progress: false - # - # - name: 🍞 Setup Bun - # uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - # with: - # bun-version: latest - # - # - name: 💾 Cache Bun dependencies - # uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - # with: - # path: ~/.bun/install/cache - # key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} - # restore-keys: | - # ${{ runner.os }}-bun-cache - # - # - name: 📦 Install dependencies and reload submodules - # run: | - # bun install --frozen-lockfile - # bun run submodule-reload - # - # - name: 🛠️ Generate project files - # run: bun run prebuild:tv - # - # - name: 🔧 Setup Xcode - # uses: maxim-lobanov/setup-xcode@v1 - # with: - # xcode-version: '26.0.1' - # - # - name: 🏗️ Setup EAS - # uses: expo/expo-github-action@main - # with: - # eas-version: latest - # token: ${{ secrets.EXPO_TOKEN }} - # eas-cache: true - # - # - name: 🚀 Build iOS app - # env: - # EXPO_TV: 1 - # run: eas build -p ios --local --non-interactive - # - # - name: 📅 Set date tag - # run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV - # - # - name: 📤 Upload IPA artifact - # uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - # with: - # name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }} - # path: build-*.ipa - # retention-days: 7 + build-ios-tv: + if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) + runs-on: macos-26 + name: 🍎 Build tvOS IPA + permissions: + contents: read + + steps: + - name: 📥 Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + submodules: recursive + show-progress: false + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 + with: + bun-version: latest + + - name: 💾 Cache Bun dependencies + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-cache + + - name: 📦 Install dependencies and reload submodules + run: | + bun install --frozen-lockfile + bun run submodule-reload + + - name: 🛠️ Generate project files + run: bun run prebuild:tv + + - name: 🔧 Setup Xcode + uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1 + with: + xcode-version: "26.2" + + - name: 🏗️ Setup EAS + uses: expo/expo-github-action@main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true + + - name: 🚀 Build iOS app + env: + EXPO_TV: 1 + run: eas build -p ios --local --non-interactive + + - name: 📅 Set date tag + run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV + + - name: 📤 Upload IPA artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }} + path: build-*.ipa + retention-days: 7 + + build-ios-tv-unsigned: + if: (!contains(github.event.head_commit.message, '[skip ci]')) + runs-on: macos-26 + name: 🍎 Build tvOS IPA (Unsigned) + permissions: + contents: read + + steps: + - name: 📥 Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + submodules: recursive + show-progress: false + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 + with: + bun-version: latest + + - name: 💾 Cache Bun dependencies + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-cache + + - name: 📦 Install dependencies and reload submodules + run: | + bun install --frozen-lockfile + bun run submodule-reload + + - name: 🛠️ Generate project files + run: bun run prebuild:tv + + - name: 🔧 Setup Xcode + uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1 + with: + xcode-version: "26.2" + + - name: 🚀 Build iOS app + env: + EXPO_TV: 1 + run: bun run ios:unsigned-build:tv ${{ github.event_name == 'pull_request' && '-- --verbose' || '' }} + + - name: 📅 Set date tag + run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV + + - name: 📤 Upload IPA artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: streamyfin-ios-tv-unsigned-ipa-${{ env.DATE_TAG }} + path: build/*.ipa + retention-days: 7 diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index c73cad70..3ebeb1b8 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -27,6 +27,13 @@ jobs: with: fetch-depth: 0 + - name: 🔑 Generate GitHub App Token + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.CROWDIN_APP_ID }} + private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }} + - name: 🌐 Sync Translations with Crowdin uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2.14.0 with: @@ -46,6 +53,6 @@ jobs: # Commit customization commit_message: "feat(i18n): update translations from Crowdin" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.generate-token.outputs.token }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/package.json b/package.json index 9864985a..34f0ff24 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "android:tv": "cross-env EXPO_TV=1 expo run:android", "build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease", "ios:unsigned-build": "cross-env EXPO_TV=0 bun scripts/ios/build-ios.ts --production", + "ios:unsigned-build:tv": "cross-env EXPO_TV=1 bun scripts/ios/build-ios.ts --production", "prepare": "husky", "typecheck": "node scripts/typecheck.js", "check": "biome check . --max-diagnostics 1000",