diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index af86644d..365afca2 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -75,10 +75,13 @@ body: id: version attributes: label: Streamyfin Version - description: What version of Streamyfin are you using? + description: What version of Streamyfin are you running? On a TestFlight or development build, choose "TestFlight/Development build" and include the exact version string shown in the app's Settings. options: - 0.54.1 - 0.51.0 + - 0.47.1 + - 0.30.2 + - 0.28.0 - Older - TestFlight/Development build validations: diff --git a/.github/actions/refresh-pr-comment/action.yml b/.github/actions/refresh-pr-comment/action.yml new file mode 100644 index 00000000..3149ea42 --- /dev/null +++ b/.github/actions/refresh-pr-comment/action.yml @@ -0,0 +1,21 @@ +name: Refresh PR build comment +description: >- + Nudge artifact-comment.yml (via workflow_dispatch) so the PR build-status + comment reflects live per-platform progress as each build job finishes. + +runs: + using: composite + steps: + # workflow_dispatch fires even when triggered by the GITHUB_TOKEN, and + # artifact-comment's concurrency group collapses simultaneous nudges, so + # this can't spam the comment. Skipped on forks (their read-only token + # cannot dispatch). github.token is used because composite actions cannot + # read the secrets context. + - if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true + shell: bash + env: + GH_TOKEN: ${{ github.token }} + HEAD_REF: ${{ github.head_ref }} + REPO: ${{ github.repository }} + run: gh workflow run artifact-comment.yml --ref "$HEAD_REF" -R "$REPO" diff --git a/.github/renovate.json b/.github/renovate.json index 45c62042..21c5b931 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -30,9 +30,17 @@ "customType": "regex", "managerFilePatterns": ["/\\.ya?ml$/"], "matchStrings": [ - "# renovate: datasource=(?\\S+) depName=(?\\S+)(?: versioning=(?\\S+))?\\s+xcode-version:\\s*[\"']?(?[^\"'\\s]+)" + "# renovate: datasource=(?\\S+) depName=(?\\S+)(?: versioning=(?\\S+))?\\s+[A-Za-z0-9._-]+:\\s*[\"']?(?[^\"'\\s]+)" ], "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}loose{{/if}}" + }, + { + "customType": "regex", + "description": "Track the Bun version pinned in eas.json build profiles (strict JSON can't hold inline annotations)", + "managerFilePatterns": ["/(^|/)eas\\.json$/"], + "matchStrings": ["\"bun\"\\s*:\\s*\"(?[^\"]+)\""], + "datasourceTemplate": "npm", + "depNameTemplate": "bun" } ], "customDatasources": { diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 8c7d5331..80c7119e 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -18,7 +18,7 @@ jobs: comment-artifacts: if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') name: 📦 Post Build Artifacts - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 permissions: contents: read pull-requests: write @@ -144,7 +144,7 @@ jobs: ) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - console.log(`Found ${buildRuns.length} non-cancelled build workflow runs for this commit`); + console.log(`Found ${buildRuns.length} build workflow runs for this commit`); // Log current status of each build for debugging buildRuns.forEach(run => { @@ -184,21 +184,35 @@ jobs: const latestAndroidRun = findBestRun('Android APK Build'); const latestIOSRun = findBestRun('iOS IPA Build'); + // Map our build targets to their job display names. Exact name is + // tried first so a signed target never collides with its + // "(Unsigned)" sibling (whose name contains the signed name). + const jobMappings = { + 'Android Phone': ['🤖 Build Android APK (Phone)'], + 'Android TV': ['🤖 Build Android APK (TV)'], + 'iOS': ['🍎 Build iOS IPA (Phone)'], + 'iOS Unsigned': ['🍎 Build iOS IPA (Phone - Unsigned)'], + 'tvOS': ['🍎 Build tvOS IPA'], + 'tvOS Unsigned': ['🍎 Build tvOS IPA (Unsigned)'] + }; + + // Prefer an exact name match over a substring match so + // '...(Phone)' doesn't swallow '...(Phone - Unsigned)'. + const findJobForTarget = (jobs, jobNames) => + jobs.find(j => jobNames.some(name => j.name === name)) || + jobs.find(j => jobNames.some(name => j.name.includes(name))); + + // Format a millisecond duration as "Xm Ys". + const fmtDuration = (ms) => { + const min = Math.floor(ms / 60000); + const sec = Math.floor((ms % 60000) / 1000); + return `${min}m ${sec}s`; + }; + // For the consolidated workflow, get individual job statuses if (latestAppsRun) { console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`); - // Map job names to our build targets. Declared outside the try so - // the catch fallback can reuse the same keys. - const jobMappings = { - 'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'], - 'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'], - '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'] - }; - try { // Get all jobs for this workflow run const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ @@ -229,10 +243,8 @@ jobs: // Create individual status for each job for (const [platform, jobNames] of Object.entries(jobMappings)) { - const job = jobs.jobs.find(j => - jobNames.some(name => j.name.includes(name) || j.name === name) - ); - + const job = findJobForTarget(jobs.jobs, jobNames); + if (job) { buildStatuses[platform] = { name: job.name, @@ -358,6 +370,43 @@ jobs: console.log(`- Artifact: ${artifact.name} (from run ${artifact.workflow_run.id})`); }); + // Pull per-job durations from the latest successful develop build so + // in-progress / queued targets can show a realistic ETA instead of + // an open-ended spinner. Best-effort: any failure just drops the ETA. + let referenceDurations = {}; + try { + const { data: devRuns } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'build-apps.yml', + branch: 'develop', + status: 'success', + per_page: 1 + }); + + if (devRuns.workflow_runs.length > 0) { + const refRun = devRuns.workflow_runs[0]; + const { data: refJobs } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: refRun.id + }); + + for (const [platform, jobNames] of Object.entries(jobMappings)) { + const job = findJobForTarget(refJobs.jobs, jobNames); + if (job && job.conclusion === 'success' && job.started_at && job.completed_at) { + referenceDurations[platform] = new Date(job.completed_at) - new Date(job.started_at); + } + } + console.log(`Reference durations from develop run ${refRun.id}:`, + Object.fromEntries(Object.entries(referenceDurations).map(([k, v]) => [k, fmtDuration(v)]))); + } else { + console.log('No successful develop build found for ETA reference'); + } + } catch (error) { + console.log('Failed to fetch develop reference durations:', error.message); + } + // Build comment body with progressive status for individual builds let commentBody = `## 🔧 Build Status for PR #${pr.number}\n\n`; commentBody += `🔗 **Commit**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\n\n`; // Progressive build status and downloads table @@ -369,9 +418,9 @@ jobs: const buildTargets = [ { 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', artifactPattern: /ios.*phone.*ipa(?!.*unsigned)/i }, + { name: 'iOS', platform: '🍎', device: '📱 Phone', statusKey: 'iOS', artifactPattern: /^(?!.*unsigned).*ios.*phone.*ipa/i }, { name: 'iOS Unsigned', platform: '🍎', device: '📱 Phone Unsigned', statusKey: 'iOS Unsigned', artifactPattern: /ios.*phone.*unsigned/i }, - { name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /ios.*tv.*ipa(?!.*unsigned)/i }, + { name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /^(?!.*unsigned).*ios.*tv.*ipa/i }, { name: 'tvOS Unsigned', platform: '🍎', device: '📺 TV Unsigned', statusKey: 'tvOS Unsigned', artifactPattern: /ios.*tv.*unsigned/i } ]; @@ -387,11 +436,12 @@ jobs: let status = '⏳ Pending'; let downloadLink = '*Waiting for build...*'; - // tvOS builds are temporarily disabled until feat/tv-interface - // is merged - show them as disabled instead of stuck pending. - if (target.name === 'tvOS' || target.name === 'tvOS Unsigned') { + // Signed tvOS stays disabled until EAS has tvOS provisioning + // profiles (app + TopShelf targets); non-interactive builds can't + // create them. Unsigned tvOS builds, so it flows through normally. + if (target.name === 'tvOS') { status = '💤 Disabled'; - downloadLink = '*Disabled until feat/tv-interface is merged*'; + downloadLink = '*Disabled — signed tvOS needs EAS provisioning profiles*'; } else if (matchingStatus) { if (matchingStatus.conclusion === 'success' && matchingArtifact) { status = '✅ Complete'; @@ -406,11 +456,9 @@ jobs: 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`; + durationInfo = ` - ${fmtDuration(durationMs)}`; } - + downloadLink = `[📥 Download ${fileType}](${directLink}) ${sizeInfo}${durationInfo}`; } else if (matchingStatus.conclusion === 'failure') { status = `❌ [Failed](${matchingStatus.url})`; @@ -420,10 +468,16 @@ jobs: downloadLink = '*Build cancelled*'; } else if (matchingStatus.status === 'in_progress') { status = `🔄 [Building...](${matchingStatus.url})`; - downloadLink = '*Build in progress...*'; + const ref = referenceDurations[target.statusKey]; + downloadLink = ref + ? `*Building… ~${fmtDuration(ref)} (avg on develop)*` + : '*Build in progress...*'; } else if (matchingStatus.status === 'queued') { status = `⏳ [Queued](${matchingStatus.url})`; - downloadLink = '*Waiting to start...*'; + const ref = referenceDurations[target.statusKey]; + downloadLink = ref + ? `*Waiting to start… ~${fmtDuration(ref)} once running (avg on develop)*` + : '*Waiting to start...*'; } else if (matchingStatus.status === 'completed' && !matchingStatus.conclusion) { // Workflow completed but conclusion not yet available (rare edge case) status = `🔄 [Finishing...](${matchingStatus.url})`; @@ -444,7 +498,22 @@ jobs: commentBody += `\n`; - // Show installation instructions if we have any artifacts + // Static rundown of the build optimisations + what each artifact + // installs on. Always shown (even mid-build) so testers know what + // to expect before downloads are ready. + commentBody += `
\n`; + commentBody += `📦 Build details & device compatibility\n\n`; + commentBody += `These CI builds are trimmed for size and speed. What that means for installing them:\n\n`; + commentBody += `| Artifact | Architectures | Installs on |\n`; + commentBody += `|---|---|---|\n`; + commentBody += `| 🤖 Android Phone APK | \`arm64-v8a\` | Every 64-bit Android phone (all since ~2017). **Not** an x86_64 emulator or a 32-bit device. |\n`; + commentBody += `| 📺 Android TV APK | \`arm64-v8a\` + \`armeabi-v7a\` | Modern boxes **and** older / cheap 32-bit Android TV sticks. No x86_64. |\n`; + commentBody += `| 🍎 iOS / tvOS IPA | \`arm64\` | iPhone / Apple TV (all current devices). |\n\n`; + commentBody += `**Why no x86_64?** That slice only runs on Android emulators / Chromebooks, never a real phone or TV box — dropping it shrinks the APK and speeds up the build. Local \`bun run android\` is unaffected (it still builds x86_64 from \`app.json\`).\n\n`; + commentBody += `**Runners:** Android on \`ubuntu-26.04\`; iOS / tvOS on Apple Silicon (\`macos-26\`). The size/speed win comes from the ABI trim above, not the runner.\n`; + commentBody += `
\n\n`; + + // Installation instructions only matter once something is downloadable. if (allArtifacts.length > 0) { commentBody += `### 🔧 Installation Instructions\n\n`; commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`; diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index c3fee61b..15a7b03a 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -23,10 +23,11 @@ env: jobs: build-android-phone: if: (!contains(github.event.head_commit.message, '[skip ci]')) - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 name: 🤖 Build Android APK (Phone) permissions: contents: read + actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - name: 🗑️ Free Disk Space @@ -37,12 +38,12 @@ jobs: android: false dotnet: true haskell: true - large-packages: true + large-packages: false docker-images: true swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -52,45 +53,58 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-bun-develop - ${{ runner.os }}-bun-develop + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 📦 Install dependencies and reload submodules run: | bun install --frozen-lockfile bun run submodule-reload + - name: ☕ Set up JDK 17 + # ubuntu-26.04 defaults to JDK 25, which breaks the RN/AGP native build + # (Kotlin falls back to JVM_23, the foojay toolchain + CMake configure + # fail). Pin Temurin 17 for a deterministic Android build. + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + with: + distribution: temurin + java-version: "17" + - name: 💾 Cache Gradle global - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: | - ~/.gradle/caches + ~/.gradle/caches/modules-2 ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | - ${{ runner.os }}-gradle- + ${{ runner.os }}-${{ runner.arch }}-gradle- - name: 🛠️ Generate project files run: bun run prebuild - name: 💾 Cache project Gradle (.gradle) - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: android/.gradle - key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-android-gradle-develop + key: ${{ runner.os }}-${{ runner.arch }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-${{ runner.arch }}-android-gradle-develop - name: 🚀 Build APK env: EXPO_TV: 0 + # CI artifact ships arm64 only (phones; emulators/Chromebooks not a + # sideload target). Overrides app.json buildArchs for this build only, + # so local `bun run android` (x86_64 emulator) is unaffected. + ORG_GRADLE_PROJECT_reactNativeArchitectures: arm64-v8a run: bun run build:android:local - name: 📅 Set date tag @@ -104,12 +118,16 @@ jobs: android/app/build/outputs/apk/release/*.apk retention-days: 7 + - name: 🔄 Refresh PR build comment + uses: ./.github/actions/refresh-pr-comment + build-android-tv: if: (!contains(github.event.head_commit.message, '[skip ci]')) - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 name: 🤖 Build Android APK (TV) permissions: contents: read + actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - name: 🗑️ Free Disk Space @@ -120,12 +138,12 @@ jobs: android: false dotnet: true haskell: true - large-packages: true + large-packages: false docker-images: true swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -135,45 +153,57 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-bun-develop - ${{ runner.os }}-bun-develop + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 📦 Install dependencies and reload submodules run: | bun install --frozen-lockfile bun run submodule-reload + - name: ☕ Set up JDK 17 + # ubuntu-26.04 defaults to JDK 25, which breaks the RN/AGP native build + # (Kotlin falls back to JVM_23, the foojay toolchain + CMake configure + # fail). Pin Temurin 17 for a deterministic Android build. + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + with: + distribution: temurin + java-version: "17" + - name: 💾 Cache Gradle global - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: | - ~/.gradle/caches + ~/.gradle/caches/modules-2 ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | - ${{ runner.os }}-gradle- + ${{ runner.os }}-${{ runner.arch }}-gradle- - name: 🛠️ Generate project files run: bun run prebuild:tv - name: 💾 Cache project Gradle (.gradle) - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: android/.gradle - key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-android-gradle-develop + key: ${{ runner.os }}-${{ runner.arch }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-${{ runner.arch }}-android-gradle-develop - name: 🚀 Build APK env: EXPO_TV: 1 + # TV artifact keeps armeabi-v7a too: many older/cheap Android TV boxes + # and sticks are still 32-bit ARM. Drops only x86_64. CI build only. + ORG_GRADLE_PROJECT_reactNativeArchitectures: arm64-v8a,armeabi-v7a run: bun run build:android:local - name: 📅 Set date tag @@ -187,16 +217,20 @@ jobs: android/app/build/outputs/apk/release/*.apk retention-days: 7 + - name: 🔄 Refresh PR build comment + uses: ./.github/actions/refresh-pr-comment + build-ios-phone: 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 (Phone) permissions: contents: read + actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -206,15 +240,16 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | - ${{ runner.os }}-bun-cache + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 📦 Install dependencies and reload submodules run: | @@ -254,16 +289,20 @@ jobs: path: build-*.ipa retention-days: 7 + - name: 🔄 Refresh PR build comment + uses: ./.github/actions/refresh-pr-comment + build-ios-phone-unsigned: if: (!contains(github.event.head_commit.message, '[skip ci]')) runs-on: macos-26 name: 🍎 Build iOS IPA (Phone - Unsigned) permissions: contents: read + actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -273,15 +312,16 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | - ${{ runner.os }}-bun-cache + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 📦 Install dependencies and reload submodules run: | @@ -312,18 +352,24 @@ jobs: path: build/*.ipa retention-days: 7 + - name: 🔄 Refresh PR build comment + uses: ./.github/actions/refresh-pr-comment + build-ios-tv: - # Temporarily disabled until feat/tv-interface is merged (TV UI not ready). - # Re-enable by removing the `false &&` prefix below. + # Disabled: EAS has no provisioning profiles / distribution cert for the tvOS + # targets (app + StreamyfinTopShelf extension), so non-interactive signed + # builds fail. Set up tvOS credentials in EAS (`eas credentials`), then remove + # the `false &&` prefix below. Unsigned tvOS builds run (see job below). if: false && (!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 + actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -333,15 +379,16 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | - ${{ runner.os }}-bun-cache + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 📦 Install dependencies and reload submodules run: | @@ -388,10 +435,11 @@ jobs: name: 🍎 Build tvOS IPA (Unsigned) permissions: contents: read + actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -401,15 +449,16 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | - ${{ runner.os }}-bun-cache + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 📦 Install dependencies and reload submodules run: | @@ -439,3 +488,6 @@ jobs: name: streamyfin-ios-tv-unsigned-ipa-${{ env.DATE_TAG }} path: build/*.ipa retention-days: 7 + + - name: 🔄 Refresh PR build comment + uses: ./.github/actions/refresh-pr-comment diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index 0cb8afc3..d4165055 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -13,13 +13,13 @@ concurrency: jobs: check-lockfile: name: 🔍 Check bun.lock and package.json consistency - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} show-progress: false @@ -29,14 +29,17 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: | ~/.bun/install/cache - key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 🛡️ Verify lockfile consistency run: | diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index f79cf58a..b9921780 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -8,11 +8,14 @@ on: schedule: - cron: '24 2 * * *' +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true jobs: analyze: name: 🔎 Analyze with CodeQL - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read security-events: write @@ -24,7 +27,7 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 🏁 Initialize CodeQL uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/conflict.yml b/.github/workflows/conflict.yml index de854ab6..125ad771 100644 --- a/.github/workflows/conflict.yml +++ b/.github/workflows/conflict.yml @@ -10,7 +10,7 @@ on: jobs: label: name: 🏷️ Labeling Merge Conflicts - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 if: ${{ github.repository == 'streamyfin/streamyfin' }} permissions: contents: read diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index b0ea48a2..c14fe48f 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -19,16 +19,16 @@ permissions: jobs: sync-translations: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: 📥 Checkout Repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: 🌐 Sync Translations with Crowdin - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 + uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 with: upload_sources: true upload_translations: true diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml index adb4c6cd..cab53d61 100644 --- a/.github/workflows/detect-duplicate.yml +++ b/.github/workflows/detect-duplicate.yml @@ -15,18 +15,19 @@ jobs: detect: name: 🔍 Find similar issues if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: issues: write contents: read steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 🔍 Detect duplicate issues run: bun scripts/detect-duplicate-issue.ts diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 8edb8916..f8799f26 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -15,7 +15,7 @@ jobs: validate_pr_title: name: "📝 Validate PR Title" if: github.event_name == 'pull_request' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: pull-requests: write contents: read @@ -46,12 +46,12 @@ jobs: dependency-review: name: 🔍 Vulnerable Dependencies - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read steps: - name: Checkout Repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -65,11 +65,10 @@ jobs: expo-doctor: name: 🚑 Expo Doctor Check - if: false - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: 🛒 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive @@ -78,17 +77,21 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 📦 Install dependencies (bun) run: bun install --frozen-lockfile - name: 🚑 Run Expo Doctor + # Re-enabled but non-blocking: surfaces doctor warnings in the logs + # without failing the gate (some checks are known-noisy for this setup). + continue-on-error: true run: bun expo-doctor code_quality: name: "🔍 Lint & Test (${{ matrix.command }})" - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 strategy: fail-fast: false matrix: @@ -101,7 +104,7 @@ jobs: steps: - name: "📥 Checkout PR code" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive @@ -110,12 +113,14 @@ jobs: - name: "🟢 Setup Node.js" uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: '24.x' + # renovate: datasource=node-version depName=node versioning=node + node-version: "24.18.0" - name: "🍞 Setup Bun" uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: "📦 Install dependencies" run: bun install --frozen-lockfile diff --git a/.github/workflows/notification.yml b/.github/workflows/notification.yml index cf0e4624..df9e4fa5 100644 --- a/.github/workflows/notification.yml +++ b/.github/workflows/notification.yml @@ -12,7 +12,7 @@ on: jobs: notify: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 if: github.event_name == 'pull_request' steps: - name: 🛎️ Notify Discord @@ -29,7 +29,7 @@ jobs: 🔗 ${{ github.event.pull_request.html_url }} notify-on-failure: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' steps: - name: 🚨 Notify Discord on Failure diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c06e8b34..027eab0d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,8 +22,9 @@ on: jobs: approve: name: 🔐 Approve release - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 environment: production + permissions: {} steps: - name: ✅ Release approved run: echo "Release approved for ${{ github.sha }}" @@ -31,7 +32,7 @@ jobs: build: name: 🚀 ${{ matrix.name }} needs: approve - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read strategy: @@ -63,7 +64,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 submodules: recursive @@ -72,15 +73,16 @@ jobs: - name: 🍞 Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: latest + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: 💾 Cache Bun dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} restore-keys: | - ${{ runner.os }}-bun-cache + ${{ runner.os }}-${{ runner.arch }}-bun- - name: 📦 Install dependencies and reload submodules run: | @@ -176,13 +178,13 @@ jobs: name: 📦 Draft GitHub Release needs: build if: ${{ !cancelled() }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: write actions: read # required for `gh run download` to list/fetch this run's artifacts steps: - name: 📥 Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 show-progress: false diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index 4972e14f..2e0f307b 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -21,27 +21,17 @@ concurrency: jobs: trivy: name: 🔎 Filesystem scan - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read security-events: write # upload SARIF to code scanning steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - # Rotate the DB cache weekly (matches the scheduled scan): cache hits within the week - # instead of a fresh immutable entry per run, still refreshing the DB every week. - - name: 🗓️ Compute weekly Trivy cache key - id: trivy-cache-key - run: echo "value=trivy-db-${{ runner.os }}-$(date -u +%G-%V)" >> "$GITHUB_OUTPUT" - - - name: 💾 Cache Trivy vulnerability DB - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: ~/.cache/trivy - key: ${{ steps.trivy-cache-key.outputs.value }} - restore-keys: trivy-db-${{ runner.os }}- + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + # Trivy's own action caches the vulnerability DB + binary internally + # (cache-trivy-* / trivy-binary-* entries), so no manual ~/.cache/trivy + # step is needed — it only duplicated the cache. - name: 🔎 Run Trivy filesystem scan uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index a23ecdf2..7f1ace97 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -1,67 +1,103 @@ -name: 🐛 Update Bug Report Template +name: 🐛 Update Issue Form Versions on: release: - types: [published] # Run on every published release on any branch + # Only full releases populate the dropdown (no drafts/prereleases). + types: [released] + schedule: + - cron: "0 3 * * 1" # Weekly safety net (Mondays 03:00 UTC) in case a release event was missed + workflow_dispatch: +# Fixed group so a release event and the weekly cron can't race on the same +# ci/update-issue-form branch — runs queue instead of force-pushing over each other. concurrency: - group: update-issue-form-${{ github.event.release.tag_name || github.run_id }} - cancel-in-progress: true + group: update-issue-form + cancel-in-progress: false + +permissions: + contents: read jobs: - update-bug-report: + update-issue-form: + name: 🔢 Populate version dropdown + runs-on: ubuntu-26.04 permissions: contents: write pull-requests: write - issues: write - runs-on: ubuntu-24.04 - steps: - name: 📥 Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: "🟢 Setup Node.js" - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - node-version: '24.x' - cache: 'npm' + # On `release` events GITHUB_SHA is the tagged commit — without this the + # script would regenerate the form from the tag's (stale) copy and the bot + # PR would revert any form edits made on develop since that release. + ref: develop - - name: 🔍 Extract minor version from app.json - id: minor - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # main + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - result-encoding: string - script: | - const fs = require('fs-extra'); - const semver = require('semver'); - const content = fs.readJsonSync('./app.json'); - const version = content.expo.version; - const minorVersion = semver.minor(version); - return minorVersion.toString(); + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - - name: 📝 Update bug report version - uses: ShaMan123/gha-populate-form-version@be012141ca560dbb92156e3fe098c46035f6260d #v2.0.5 - with: - semver: '^0.${{ steps.minor.outputs.result }}.0' - dry_run: no-push + - name: 🔢 Populate version dropdown from GitHub releases + id: populate + run: bun scripts/update-issue-form.mjs + env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} - - name: ⚙️ Update bug report node version dropdown - uses: ShaMan123/gha-populate-form-version@be012141ca560dbb92156e3fe098c46035f6260d #v2.0.5 - with: - dropdown: _node_version - package: node - semver: '>=24.0.0' - dry_run: no-push - - - name: 📬 Commit and create pull request + - name: 📬 Create pull request + id: cpr uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: - add-paths: .github/ISSUE_TEMPLATE/bug_report.yml - branch: ci-update-bug-report + add-paths: .github/ISSUE_TEMPLATE/issue_report.yml + branch: ci/update-issue-form base: develop delete-branch: true labels: ⚙️ ci, 🤖 github-actions - title: 'chore(): Update bug report template to match release version' + commit-message: "chore: update issue form version dropdown" + title: "chore: update issue form version dropdown" + # Follows .github/pull_request_template.md so the bot PR isn't flagged by PR validation. body: | - Automated update to `.github/ISSUE_TEMPLATE/bug_report.yml` - Triggered by workflow run [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + # 📦 Pull Request + + ## 📝 Description + + Automated update of the **Streamyfin Version** dropdown in `.github/ISSUE_TEMPLATE/issue_report.yml`, populated from the latest published GitHub releases by `scripts/update-issue-form.mjs`. + + **Version dropdown now lists:** ${{ steps.populate.outputs.versions }} + + Triggered by `${{ github.event_name }}`${{ github.event.release.tag_name && format(' — release {0}', github.event.release.tag_name) || '' }} · [run ${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). + + ## 🏷️ Ticket / Issue + + N/A — automated maintenance. + + ### 🖼️ Screenshots / GIFs (if UI) + + N/A — issue-template metadata only, no app UI. + + ## ✅ Checklist + + - [x] I’ve read the [contribution guidelines](CONTRIBUTING.md) + - [x] Verified that changes behave as expected for all platforms + - [x] Code passes lint/formatting and type checks (`tsc`/`biome`) + - [x] No secrets, hardcoded credentials, or private config files are included + - [x] I've declared if AI was used to assist with this PR (by uncommenting the line at the bottom, or not) + + ## 🔍 Testing Instructions + + N/A — generated by CI from published releases; review the dropdown diff in `issue_report.yml`. + + - name: 🔀 Enable auto-merge + if: steps.cpr.outputs.pull-request-operation == 'created' + env: + GH_TOKEN: ${{ github.token }} + # Known limitation: PRs created with GITHUB_TOKEN don't trigger CI workflows + # (GitHub anti-recursion), so the required checks stay "Expected" until a + # maintainer kicks them (close/reopen the PR, or push an empty commit). + # Auto-merge is still worth enabling: once checks run and reviews land, + # the PR merges itself. + run: | + gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}" \ + || echo "::warning::Could not enable auto-merge — enable 'Allow auto-merge' in repo settings (and branch protection); merge the PR manually for now." diff --git a/GLOBAL_MODAL_GUIDE.md b/GLOBAL_MODAL_GUIDE.md index 5426ca72..11a05f10 100644 --- a/GLOBAL_MODAL_GUIDE.md +++ b/GLOBAL_MODAL_GUIDE.md @@ -143,14 +143,6 @@ interface ModalOptions { } ``` -## Examples - -See `components/ExampleGlobalModalUsage.tsx` for comprehensive examples including: -- Simple content modal -- Modal with custom snap points -- Complex component in modal -- Success/error modals triggered from functions - ## Default Styling The modal uses these default styles (can be overridden via options): diff --git a/README.md b/README.md index 258005ef..3d4221f4 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Thanks to [@Alexk2309](https://github.com/Alexk2309) for the hard work building ## 🛣️ Roadmap -Check out our [Roadmap](https://github.com/users/fredrikburmester/projects/5) To see what we're working on next, we are always open to feedback and suggestions. Please let us know if you have any ideas or feature requests. +Check out our [Roadmap](https://github.com/orgs/streamyfin/projects/3/views/1) to see what we're working on next, we are always open to feedback and suggestions. Please let us know if you have any ideas or feature requests. ## 📥 Download Streamyfin diff --git a/app.json b/app.json index 296d674d..e7095490 100644 --- a/app.json +++ b/app.json @@ -107,6 +107,9 @@ ], "expo-localization", "expo-asset", + "expo-audio", + "expo-image", + "expo-sharing", [ "react-native-edge-to-edge", { diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 29461b49..759dae85 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -305,6 +305,8 @@ export default function SearchPage() { }, hideWhenScrolling: false, autoFocus: false, + // Android: color of the user-typed text (was dark and unreadable on the dark header) + textColor: "#fff", // Android: placeholder and icon color hintTextColor: "#fff", headerIconColor: "#fff", diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 53fbeb91..45f246a5 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -3,16 +3,24 @@ import { type NativeBottomTabNavigationEventMap, type NativeBottomTabNavigationOptions, } from "@bottom-tabs/react-navigation"; -import { withLayoutContext } from "expo-router"; +import { Stack, useSegments, withLayoutContext } from "expo-router"; import type { ParamListBase, TabNavigationState, } from "expo-router/react-navigation"; +import { useCallback, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Platform, View } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; +import type { TVNavBarTab } from "@/components/tv/TVNavBar"; +import { TVNavBar } from "@/components/tv/TVNavBar"; import { Colors } from "@/constants/Colors"; -import { useTVHomeBackHandler } from "@/hooks/useTVBackHandler"; +import useRouter from "@/hooks/useAppRouter"; +import { + isTabRoute, + useTVHomeBackHandler, + useTVTabRootBackHandler, +} from "@/hooks/useTVBackHandler"; import { useSettings } from "@/utils/atoms/settings"; import { eventBus } from "@/utils/eventBus"; @@ -33,13 +41,108 @@ export const NativeTabs = withLayoutContext< NativeBottomTabNavigationEventMap >(Navigator); +const IS_ANDROID_TV = Platform.isTV && Platform.OS === "android"; + +function TVTabLayout() { + const { settings } = useSettings(); + const { t } = useTranslation(); + const segments = useSegments(); + const router = useRouter(); + + const currentTab = segments.find(isTabRoute); + const lastSegment = segments[segments.length - 1] ?? ""; + const atTabRoot = isTabRoute(lastSegment) || lastSegment === "index"; + + const tabs: TVNavBarTab[] = useMemo( + () => + [ + { key: "(home)", label: t("tabs.home") }, + { key: "(search)", label: t("tabs.search") }, + { key: "(favorites)", label: t("tabs.favorites") }, + !settings?.streamyStatsServerUrl || settings?.hideWatchlistsTab + ? null + : { key: "(watchlists)", label: t("watchlists.title") }, + { key: "(libraries)", label: t("tabs.library") }, + !settings?.showCustomMenuLinks + ? null + : { key: "(custom-links)", label: t("tabs.custom_links") }, + { key: "(settings)", label: t("tabs.settings") }, + ].filter((tab): tab is TVNavBarTab => tab !== null), + [ + settings?.streamyStatsServerUrl, + settings?.hideWatchlistsTab, + settings?.showCustomMenuLinks, + t, + ], + ); + + const activeTabKey = currentTab ?? "(home)"; + + const visibleKeys = useMemo( + () => new Set(tabs.map((tab) => tab.key)), + [tabs], + ); + + const handleTabChange = useCallback( + (key: string) => { + if (key === currentTab) return; + + if (key === "(home)") eventBus.emit("scrollToTop"); + if (key === "(search)") eventBus.emit("searchTabPressed"); + + router.replace(`/(auth)/(tabs)/${key}`); + }, + [currentTab, router], + ); + + const navigateHome = useCallback(() => { + router.replace("/(auth)/(tabs)/(home)"); + }, [router]); + useTVTabRootBackHandler(navigateHome, atTabRoot, currentTab); + + // If current tab is no longer visible (setting changed), navigate to home + useEffect(() => { + if (!visibleKeys.has(activeTabKey) && activeTabKey !== "(home)") { + router.replace("/(auth)/(tabs)/(home)"); + } + }, [visibleKeys, activeTabKey, router]); + + return ( + + + ); +} + export default function TabLayout() { const { settings } = useSettings(); const { t } = useTranslation(); - // Handle TV back button - prevent app exit when at root + // Must be called before any conditional return (rules of hooks) useTVHomeBackHandler(); + if (IS_ANDROID_TV) { + return ; + } + return (