From 680f930a476c9250091a1d69c17c7720bc25aaaa Mon Sep 17 00:00:00 2001 From: Gauvain Date: Tue, 7 Jul 2026 11:46:35 +0200 Subject: [PATCH 1/2] fix(tv): re-negotiate the stream when changing audio track while transcoding (#1791) --- app/(auth)/player/direct-player.tsx | 30 ++++++++++++++++++- app/(auth)/tv-option-modal.tsx | 15 ++++++++++ .../video-player/controls/Controls.tv.tsx | 3 ++ hooks/useTVOptionModal.ts | 2 ++ utils/atoms/tvOptionModal.ts | 9 ++++++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 877dd1637..515e39d15 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -893,6 +893,27 @@ export default function DirectPlayerPage() { // Check if we're transcoding const isTranscoding = Boolean(stream?.mediaSource?.TranscodingUrl); + // A transcoded stream only carries the audio track the server encoded + // into it โ€” switching requires re-negotiating the stream with the new + // index (like the mobile menu's replacePlayer), not an mpv aid change. + if (isTranscoding) { + const queryParams = new URLSearchParams({ + itemId: item?.Id ?? "", + audioIndex: String(index), + subtitleIndex: String(currentSubtitleIndex), + mediaSourceId: stream?.mediaSource?.Id ?? "", + bitrateValue: bitrateValue?.toString() ?? "", + playbackPosition: msToTicks(progress.get()).toString(), + }).toString(); + // Destroy the current mpv instance BEFORE navigating, same rationale as + // goToNextItem/goToPreviousItem: Expo Router briefly holds two players + // during the transition, and two simultaneous decoders OOM-kill low-RAM + // devices. Resume is preserved via the playbackPosition param. + videoRef.current?.destroy().catch(() => {}); + router.replace(`player/direct-player?${queryParams}` as any); + return; + } + // Convert Jellyfin index to MPV track ID const mpvTrackId = getMpvAudioId( stream?.mediaSource, @@ -904,7 +925,14 @@ export default function DirectPlayerPage() { await videoRef.current?.setAudioTrack?.(mpvTrackId); } }, - [stream?.mediaSource], + [ + stream?.mediaSource, + item?.Id, + currentSubtitleIndex, + bitrateValue, + router, + progress, + ], ); // TV subtitle track change handler diff --git a/app/(auth)/tv-option-modal.tsx b/app/(auth)/tv-option-modal.tsx index 180228e36..442a29ec5 100644 --- a/app/(auth)/tv-option-modal.tsx +++ b/app/(auth)/tv-option-modal.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Animated, Easing, + InteractionManager, ScrollView, StyleSheet, TVFocusGuideView, @@ -75,6 +76,20 @@ export default function TVOptionModal() { }, [isReady]); const handleSelect = (value: any) => { + if (modalState?.deferApplyUntilDismissed) { + // onSelect navigates (the transcode audio switch replacing the player); + // a router.replace fired while this modal is the active route would be + // swallowed. Close FIRST, apply after dismissal. + const onSelect = modalState.onSelect; + store.set(tvOptionModalAtom, null); + router.back(); + InteractionManager.runAfterInteractions(() => onSelect?.(value)); + return; + } + // State-only callers (detail page, library filters, settings): run before + // closing so the re-render happens while the modal is up. Deferring it until + // after dismissal re-renders the page after focus returns and yanks TV + // focus, leaving navigation stuck. modalState?.onSelect(value); store.set(tvOptionModalAtom, null); router.back(); diff --git a/components/video-player/controls/Controls.tv.tsx b/components/video-player/controls/Controls.tv.tsx index a85f62154..29e5d1f03 100644 --- a/components/video-player/controls/Controls.tv.tsx +++ b/components/video-player/controls/Controls.tv.tsx @@ -564,6 +564,9 @@ export const Controls: FC = ({ title: t("item_card.audio"), options: audioOptions, onSelect: handleAudioChange, + // In-player audio selection navigates (replacePlayer while transcoding); + // apply it after the modal is dismissed so it isn't swallowed. + deferApplyUntilDismissed: true, }); controlsInteractionRef.current(); }, [showOptions, t, audioOptions, handleAudioChange]); diff --git a/hooks/useTVOptionModal.ts b/hooks/useTVOptionModal.ts index c6acffe83..68db4de8d 100644 --- a/hooks/useTVOptionModal.ts +++ b/hooks/useTVOptionModal.ts @@ -12,6 +12,7 @@ interface ShowOptionsParams { onSelect: (value: T) => void; cardWidth?: number; cardHeight?: number; + deferApplyUntilDismissed?: boolean; } export const useTVOptionModal = () => { @@ -26,6 +27,7 @@ export const useTVOptionModal = () => { onSelect: params.onSelect, cardWidth: params.cardWidth, cardHeight: params.cardHeight, + deferApplyUntilDismissed: params.deferApplyUntilDismissed, }); router.push("/(auth)/tv-option-modal"); }, diff --git a/utils/atoms/tvOptionModal.ts b/utils/atoms/tvOptionModal.ts index 74bc2ab54..c6fe230b2 100644 --- a/utils/atoms/tvOptionModal.ts +++ b/utils/atoms/tvOptionModal.ts @@ -13,6 +13,15 @@ export type TVOptionModalState = { onSelect: (value: any) => void; cardWidth?: number; cardHeight?: number; + /** + * Run onSelect AFTER the modal route is dismissed. Needed only when onSelect + * navigates (the in-player audio switch replacing the player while + * transcoding), which the still-active modal route would otherwise swallow. + * Default (false) runs onSelect before closing, so state-only callers (detail + * page, library filters, settings) don't re-render after focus returns and + * lose TV focus. + */ + deferApplyUntilDismissed?: boolean; } | null; export const tvOptionModalAtom = atom(null); From 77156630d6de5bf678bde7515962170f67ae17e6 Mon Sep 17 00:00:00 2001 From: Gauvain Date: Tue, 7 Jul 2026 23:26:52 +0200 Subject: [PATCH 2/2] ci: run independent workflow steps in parallel (#1788) --- .github/workflows/build-apps.yml | 376 ++++++++++++------------ .github/workflows/check-lockfile.yml | 24 +- .github/workflows/detect-duplicate.yml | 15 +- .github/workflows/linting.yml | 98 +++--- .github/workflows/pr-title-comment.yml | 76 +++++ .github/workflows/pr-title.yml | 42 +++ .github/workflows/release.yml | 39 +-- .github/workflows/update-issue-form.yml | 25 +- 8 files changed, 404 insertions(+), 291 deletions(-) create mode 100644 .github/workflows/pr-title-comment.yml create mode 100644 .github/workflows/pr-title.yml diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index 4c1bbb916..0be3084f6 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -30,64 +30,66 @@ jobs: actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - - name: ๐Ÿ—‘๏ธ Free Disk Space - uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3 - with: - tool-cache: false - mandb: true - android: false - dotnet: true - haskell: true - large-packages: false - docker-images: true - swap-storage: false + - parallel: + - name: ๐Ÿ—‘๏ธ Free Disk Space + uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3 + with: + tool-cache: false + mandb: true + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-bun- + - 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" + + - parallel: + - name: ๐Ÿ’พ Cache Bun dependencies + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun- + + - name: ๐Ÿ’พ Cache Gradle global + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + with: + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-gradle- - 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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 - with: - path: | - ~/.gradle/caches/modules-2 - ~/.gradle/wrapper - key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-gradle- - - name: ๐Ÿ› ๏ธ Generate project files run: bun run prebuild @@ -130,64 +132,66 @@ jobs: actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - - name: ๐Ÿ—‘๏ธ Free Disk Space - uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3 - with: - tool-cache: false - mandb: true - android: false - dotnet: true - haskell: true - large-packages: false - docker-images: true - swap-storage: false + - parallel: + - name: ๐Ÿ—‘๏ธ Free Disk Space + uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3 + with: + tool-cache: false + mandb: true + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - - name: ๐Ÿ’พ Cache Bun dependencies - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-bun- + - 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" + + - parallel: + - name: ๐Ÿ’พ Cache Bun dependencies + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun- + + - name: ๐Ÿ’พ Cache Gradle global + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + with: + path: | + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper + key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-gradle- - 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@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 - with: - path: | - ~/.gradle/caches/modules-2 - ~/.gradle/wrapper - key: ${{ runner.os }}-${{ runner.arch }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-gradle- - - name: ๐Ÿ› ๏ธ Generate project files run: bun run prebuild:tv @@ -229,19 +233,33 @@ jobs: actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false + - parallel: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" + + - name: ๐Ÿ”ง Setup Xcode + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 + with: + # renovate: datasource=custom.xcode depName=xcode versioning=loose + xcode-version: "26.6" + + - name: ๐Ÿ—๏ธ Setup EAS + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true - name: ๐Ÿ’พ Cache Bun dependencies uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 @@ -259,19 +277,6 @@ jobs: - name: ๐Ÿ› ๏ธ Generate project files run: bun run prebuild - - name: ๐Ÿ”ง Setup Xcode - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 - with: - # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.6" - - - name: ๐Ÿ—๏ธ Setup EAS - uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - eas-cache: true - - name: ๐Ÿš€ Build iOS app env: EXPO_TV: 0 @@ -301,19 +306,26 @@ jobs: actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false + - parallel: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" + + - name: ๐Ÿ”ง Setup Xcode + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 + with: + # renovate: datasource=custom.xcode depName=xcode versioning=loose + xcode-version: "26.6" - name: ๐Ÿ’พ Cache Bun dependencies uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 @@ -331,12 +343,6 @@ jobs: - name: ๐Ÿ› ๏ธ Generate project files run: bun run prebuild - - name: ๐Ÿ”ง Setup Xcode - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 - with: - # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.6" - - name: ๐Ÿš€ Build iOS app env: EXPO_TV: 0 @@ -368,19 +374,33 @@ jobs: actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false + - parallel: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" + + - name: ๐Ÿ”ง Setup Xcode + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 + with: + # renovate: datasource=custom.xcode depName=xcode versioning=loose + xcode-version: "26.6" + + - name: ๐Ÿ—๏ธ Setup EAS + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true - name: ๐Ÿ’พ Cache Bun dependencies uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 @@ -398,19 +418,6 @@ jobs: - name: ๐Ÿ› ๏ธ Generate project files run: bun run prebuild:tv - - name: ๐Ÿ”ง Setup Xcode - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 - with: - # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.6" - - - name: ๐Ÿ—๏ธ Setup EAS - uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - eas-cache: true - - name: ๐Ÿš€ Build iOS app env: EXPO_TV: 1 @@ -438,19 +445,26 @@ jobs: actions: write # dispatch artifact-comment.yml to refresh the PR comment steps: - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false + - parallel: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" + + - name: ๐Ÿ”ง Setup Xcode + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 + with: + # renovate: datasource=custom.xcode depName=xcode versioning=loose + xcode-version: "26.6" - name: ๐Ÿ’พ Cache Bun dependencies uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 @@ -468,12 +482,6 @@ jobs: - name: ๐Ÿ› ๏ธ Generate project files run: bun run prebuild:tv - - name: ๐Ÿ”ง Setup Xcode - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 - with: - # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.6" - - name: ๐Ÿš€ Build iOS app env: EXPO_TV: 1 diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index d4165055d..d06699306 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -18,19 +18,19 @@ jobs: contents: read steps: - - name: ๐Ÿ“ฅ Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - show-progress: false - submodules: recursive - fetch-depth: 0 + - parallel: + - name: ๐Ÿ“ฅ Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + show-progress: false + submodules: recursive - - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: ๐Ÿ’พ Cache Bun dependencies uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml index cab53d615..a70b6ffb3 100644 --- a/.github/workflows/detect-duplicate.yml +++ b/.github/workflows/detect-duplicate.yml @@ -20,14 +20,15 @@ jobs: issues: write contents: read steps: - - name: ๐Ÿ“ฅ Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - parallel: + - name: ๐Ÿ“ฅ Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # 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 f8799f261..992455862 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -2,7 +2,7 @@ name: ๐Ÿšฆ Security & Quality Gate on: pull_request: - types: [opened, edited, synchronize, reopened] + types: [opened, synchronize, reopened] branches: [develop, master] workflow_dispatch: push: @@ -11,39 +11,11 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - validate_pr_title: - name: "๐Ÿ“ Validate PR Title" - if: github.event_name == 'pull_request' - runs-on: ubuntu-26.04 - permissions: - pull-requests: write - contents: read - steps: - - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 - id: lint_pr_title - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 - if: always() && (steps.lint_pr_title.outputs.error_message != null) - with: - header: pr-title-lint-error - message: | - Hey there and thank you for opening this pull request! ๐Ÿ‘‹๐Ÿผ - We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). - - **Error details:** - ``` - ${{ steps.lint_pr_title.outputs.error_message }} - ``` - - - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 - with: - header: pr-title-lint-error - delete: true - dependency-review: name: ๐Ÿ” Vulnerable Dependencies runs-on: ubuntu-26.04 @@ -67,18 +39,26 @@ jobs: name: ๐Ÿš‘ Expo Doctor Check runs-on: ubuntu-26.04 steps: - - name: ๐Ÿ›’ Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - submodules: recursive - fetch-depth: 0 + - parallel: + - name: ๐Ÿ›’ Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + submodules: recursive - - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" + + - name: ๐Ÿ’พ Cache Bun dependencies + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + path: ~/.bun/install/cache + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun- - name: ๐Ÿ“ฆ Install dependencies (bun) run: bun install --frozen-lockfile @@ -103,24 +83,26 @@ jobs: - "i18n:check" steps: - - name: "๐Ÿ“ฅ Checkout PR code" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - submodules: recursive - fetch-depth: 0 + - parallel: + - name: "๐Ÿ“ฅ Checkout PR code" + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + submodules: recursive - - name: "๐ŸŸข Setup Node.js" - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - # 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: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - - name: "๐Ÿž Setup Bun" - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + - name: ๐Ÿ’พ Cache Bun dependencies + uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + path: ~/.bun/install/cache + key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun- - name: "๐Ÿ“ฆ Install dependencies" run: bun install --frozen-lockfile diff --git a/.github/workflows/pr-title-comment.yml b/.github/workflows/pr-title-comment.yml new file mode 100644 index 000000000..a3117f269 --- /dev/null +++ b/.github/workflows/pr-title-comment.yml @@ -0,0 +1,76 @@ +name: ๐Ÿ“ PR Title Comment + +# Posts / updates / deletes the PR-title lint sticky comment with a write token, +# so it works on FORK PRs too (the pull_request-triggered pr-title.yml only has a +# read-only token on forks). workflow_run runs from the base branch with the base +# repo's token. This is comment-only โ€” the required check stays pr-title.yml. +on: + workflow_run: + workflows: ["๐Ÿ“ PR Title"] + types: [completed] + +permissions: + contents: read + actions: read # download the artifact from the triggering run + pull-requests: write # post/update/delete the sticky comment + +concurrency: + group: pr-title-comment-${{ github.event.workflow_run.id }} + cancel-in-progress: true + +jobs: + comment: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-26.04 + steps: + - name: โฌ‡๏ธ Download lint result + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh run download "${{ github.event.workflow_run.id }}" \ + --repo "${{ github.repository }}" \ + --name pr-title-result \ + --dir result + + - name: ๐Ÿ”Ž Read result + id: result + run: | + echo "number=$(cat result/number)" >> "$GITHUB_OUTPUT" + if [ -s result/error ]; then + echo "has_error=true" >> "$GITHUB_OUTPUT" + else + echo "has_error=false" >> "$GITHUB_OUTPUT" + fi + + - name: ๐Ÿ“ฅ Load error message + if: steps.result.outputs.has_error == 'true' + run: | + { + echo "LINT_ERROR<> "$GITHUB_ENV" + + - name: ๐Ÿ’ฌ Post title error + if: steps.result.outputs.has_error == 'true' + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + with: + number: ${{ steps.result.outputs.number }} + header: pr-title-lint-error + message: | + Hey there and thank you for opening this pull request! ๐Ÿ‘‹๐Ÿผ + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). + + **Error details:** + ``` + ${{ env.LINT_ERROR }} + ``` + + - name: ๐Ÿงน Clear title error + if: steps.result.outputs.has_error == 'false' + uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + with: + number: ${{ steps.result.outputs.number }} + header: pr-title-lint-error + delete: true diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 000000000..68c503d67 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,42 @@ +name: ๐Ÿ“ PR Title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + branches: [develop, master] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate_pr_title: + name: "๐Ÿ“ Validate PR Title" + runs-on: ubuntu-26.04 + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Fork PRs only get a read-only GITHUB_TOKEN here, so the sticky comment + # is posted from the privileged pr-title-comment.yml (workflow_run). Hand + # off the result (PR number + lint error) as an artifact for it to read. + - if: always() + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + LINT_ERROR: ${{ steps.lint_pr_title.outputs.error_message }} + run: | + mkdir -p pr-title-result + printf '%s' "$PR_NUMBER" > pr-title-result/number + printf '%s' "$LINT_ERROR" > pr-title-result/error + + - if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-title-result + path: pr-title-result/ + retention-days: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 027eab0de..9860d6f53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,8 @@ name: ๐Ÿš€ Release (EAS build + submit) concurrency: group: release-${{ github.ref }} cancel-in-progress: false + # Queue successive releases in order instead of dropping the extra pending run. + queue: max on: push: @@ -63,18 +65,26 @@ jobs: artifact_name: streamyfin-android-tv-apk steps: - - name: ๐Ÿ“ฅ Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - fetch-depth: 0 - submodules: recursive - show-progress: false + - parallel: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + submodules: recursive + show-progress: false - - name: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" + + - name: ๐Ÿ—๏ธ Setup EAS + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true - name: ๐Ÿ’พ Cache Bun dependencies uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 @@ -89,13 +99,6 @@ jobs: bun install --frozen-lockfile bun run submodule-reload - - name: ๐Ÿ—๏ธ Setup EAS - uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - eas-cache: true - # tvOS uses credentialsSource: local โ€” restore the gitignored # credentials.json + cert + provisioning profiles from secrets. - name: ๐Ÿ” Restore tvOS signing credentials diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index 7f1ace97f..db111d560 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -25,19 +25,20 @@ jobs: contents: write pull-requests: write steps: - - name: ๐Ÿ“ฅ Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - # 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 + - parallel: + - name: ๐Ÿ“ฅ Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # 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: ๐Ÿž Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - # renovate: datasource=npm depName=bun - bun-version: "1.3.14" + - name: ๐Ÿž Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # renovate: datasource=npm depName=bun + bun-version: "1.3.14" - name: ๐Ÿ”ข Populate version dropdown from GitHub releases id: populate