From 2a195d9ba520b79d4e97c4b4826ed461b2aa5f3f Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 25 Sep 2025 00:15:46 +0200
Subject: [PATCH 01/27] chore(deps): Update actions/cache action to v4.3.0
(#1084)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
.github/workflows/build-android.yml | 6 +++---
.github/workflows/build-ios.yml | 2 +-
.github/workflows/check-lockfile.yml | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
index 2b79e2dd..bc9b9ea1 100644
--- a/.github/workflows/build-android.yml
+++ b/.github/workflows/build-android.yml
@@ -39,7 +39,7 @@ jobs:
bun-version: latest
- name: ๐พ Cache Bun dependencies
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
@@ -53,7 +53,7 @@ jobs:
bun run submodule-reload
- name: ๐พ Cache Gradle global
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.gradle/caches
@@ -70,7 +70,7 @@ jobs:
fi
- name: ๐พ Cache project Gradle (.gradle)
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: android/.gradle
key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
index b7486da5..98b587b3 100644
--- a/.github/workflows/build-ios.yml
+++ b/.github/workflows/build-ios.yml
@@ -44,7 +44,7 @@ jobs:
bun-version: latest
- name: ๐พ Cache Bun dependencies
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }}
diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml
index 6134ad5f..cc606b19 100644
--- a/.github/workflows/check-lockfile.yml
+++ b/.github/workflows/check-lockfile.yml
@@ -32,7 +32,7 @@ jobs:
bun-version: latest
- name: ๐พ Cache Bun dependencies
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.bun/install/cache
From ead37aa8065c288dd88ccbfdbd74d4dc42e02c38 Mon Sep 17 00:00:00 2001
From: Gauvain <68083474+Gauvino@users.noreply.github.com>
Date: Thu, 25 Sep 2025 22:47:49 +0200
Subject: [PATCH 02/27] fix: resolve type issues and improve component
reliability (#1078)
---
.../jellyseerr/page.tsx | 26 ++++++---
components/common/JellyseerrItemRouter.tsx | 2 +-
components/common/TouchableItemRouter.tsx | 55 +++++++++++++++++--
components/home/LargeMovieCarousel.tsx | 9 ++-
components/inputs/PinInput.tsx | 14 ++++-
components/jellyseerr/PersonPoster.tsx | 2 +-
.../jellyseerr/discover/CompanySlide.tsx | 5 +-
components/jellyseerr/discover/GenreSlide.tsx | 5 +-
.../discover/RecentRequestsSlide.tsx | 19 ++++++-
components/series/CastAndCrew.tsx | 18 +++---
components/series/SeasonEpisodesCarousel.tsx | 3 +-
components/series/SeasonPicker.tsx | 5 +-
components/settings/HomeIndex.tsx | 8 +--
components/video-player/controls/Controls.tsx | 3 +-
.../controls/contexts/VideoContext.tsx | 3 +-
.../controls/dropdown/DropdownView.tsx | 3 +-
hooks/useJellyseerr.ts | 4 +-
translations/en.json | 23 +++++++-
utils/atoms/settings.ts | 1 +
19 files changed, 147 insertions(+), 61 deletions(-)
diff --git a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
index 7113c24f..3b076235 100644
--- a/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search,favorites)/jellyseerr/page.tsx
@@ -139,7 +139,15 @@ const Page: React.FC = () => {
}
requestMedia(mediaTitle, body, refetch);
- }, [details, result, requestMedia, hasAdvancedRequestPermission]);
+ }, [
+ details,
+ result,
+ requestMedia,
+ hasAdvancedRequestPermission,
+ mediaTitle,
+ refetch,
+ mediaType,
+ ]);
const isAnime = useMemo(
() =>
@@ -277,12 +285,16 @@ const Page: React.FC = () => {
)
diff --git a/app/_layout.tsx b/app/_layout.tsx
index c94f540e..677fc6ce 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -395,12 +395,17 @@ function Layout() {
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
- BackGroundDownloader.checkForExistingDownloads();
+ BackGroundDownloader.checkForExistingDownloads().catch(
+ (error: unknown) => {
+ writeErrorLog("Failed to resume background downloads", error);
+ },
+ );
}
});
- BackGroundDownloader.checkForExistingDownloads();
-
+ BackGroundDownloader.checkForExistingDownloads().catch((error: unknown) => {
+ writeErrorLog("Failed to resume background downloads", error);
+ });
return () => {
subscription.remove();
};
diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx
index 60a248c7..80ead77a 100644
--- a/components/DownloadItem.tsx
+++ b/components/DownloadItem.tsx
@@ -225,7 +225,7 @@ export const DownloadItems: React.FC = ({
if (!mediaSource) {
console.error(`Could not get download URL for ${item.Name}`);
toast.error(
- t("Could not get download URL for {{itemName}}", {
+ t("home.downloads.toasts.could_not_get_download_url_for_item", {
itemName: item.Name,
}),
);
diff --git a/components/downloads/DownloadCard.tsx b/components/downloads/DownloadCard.tsx
index 5ffce752..b90c4836 100644
--- a/components/downloads/DownloadCard.tsx
+++ b/components/downloads/DownloadCard.tsx
@@ -6,6 +6,7 @@ import { t } from "i18next";
import { useMemo } from "react";
import {
ActivityIndicator,
+ Platform,
TouchableOpacity,
type TouchableOpacityProps,
View,
@@ -109,9 +110,9 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
/>
)}
- {/* Action buttons in top right corner */}
-
- {process.status === "downloading" && (
+ {/* Action buttons in bottom right corner */}
+
+ {process.status === "downloading" && Platform.OS !== "ios" && (
handlePause(process.id)}
className='p-1'
@@ -119,7 +120,7 @@ export const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
)}
- {process.status === "paused" && (
+ {process.status === "paused" && Platform.OS !== "ios" && (
handleResume(process.id)}
className='p-1'
diff --git a/components/series/SeriesActions.tsx b/components/series/SeriesActions.tsx
index 64a9dbfa..a2cd32ba 100644
--- a/components/series/SeriesActions.tsx
+++ b/components/series/SeriesActions.tsx
@@ -1,6 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useCallback, useMemo } from "react";
+import { useTranslation } from "react-i18next";
import {
Alert,
Linking,
@@ -16,6 +17,7 @@ interface Props extends ViewProps {
}
export const ItemActions = ({ item, ...props }: Props) => {
+ const { t } = useTranslation();
const trailerLink = useMemo(() => {
if ("RemoteTrailers" in item && item.RemoteTrailers?.[0]?.Url) {
return item.RemoteTrailers[0].Url;
@@ -30,7 +32,7 @@ export const ItemActions = ({ item, ...props }: Props) => {
const openTrailer = useCallback(async () => {
if (!trailerLink) {
- Alert.alert("No trailer available");
+ Alert.alert(t("common.no_trailer_available"));
return;
}
@@ -39,7 +41,7 @@ export const ItemActions = ({ item, ...props }: Props) => {
} catch (err) {
console.error("Failed to open trailer link:", err);
}
- }, [trailerLink]);
+ }, [trailerLink, t]);
return (
diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx
index e0ca1147..d4d7e598 100644
--- a/components/settings/OtherSettings.tsx
+++ b/components/settings/OtherSettings.tsx
@@ -41,10 +41,10 @@ export const OtherSettings: React.FC = () => {
if (settings?.autoDownload === true && !registered) {
registerBackgroundFetchAsync();
- toast.success("Background downloads enabled");
+ toast.success(t("home.settings.toasts.background_downloads_enabled"));
} else if (settings?.autoDownload === false && registered) {
unregisterBackgroundFetchAsync();
- toast.info("Background downloads disabled");
+ toast.info(t("home.settings.toasts.background_downloads_disabled"));
} else if (settings?.autoDownload === true && registered) {
// Don't to anything
} else if (settings?.autoDownload === false && !registered) {
diff --git a/plugins/withRNBackgroundDownloader.js b/plugins/withRNBackgroundDownloader.js
index 83e1fb69..af24d642 100644
--- a/plugins/withRNBackgroundDownloader.js
+++ b/plugins/withRNBackgroundDownloader.js
@@ -50,9 +50,11 @@ function withRNBackgroundDownloader(config) {
// Expo 53's xcodeโjs doesn't expose pbxTargets().
// Setting the property once at the project level is sufficient.
["Debug", "Release"].forEach((cfg) => {
+ // Use the detected projectName to set the bridging header path instead of a hardcoded value
+ const bridgingHeaderPath = `${projectName}/${projectName}-Bridging-Header.h`;
project.updateBuildProperty(
"SWIFT_OBJC_BRIDGING_HEADER",
- "Streamyfin/Streamyfin-Bridging-Header.h",
+ bridgingHeaderPath,
cfg,
);
});
diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx
index c3180c0a..4306b8d6 100644
--- a/providers/DownloadProvider.tsx
+++ b/providers/DownloadProvider.tsx
@@ -25,7 +25,7 @@ import { useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device";
import useDownloadHelper from "@/utils/download";
import { getItemImage } from "@/utils/getItemImage";
-import { writeToLog } from "@/utils/log";
+import { dumpDownloadDiagnostics, writeToLog } from "@/utils/log";
import { storage } from "@/utils/mmkv";
import { fetchAndParseSegments } from "@/utils/segments";
import { generateTrickplayUrl, getTrickplayInfo } from "@/utils/trickplay";
@@ -42,37 +42,60 @@ const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;
+// Cap progress at 99% to avoid showing 100% before the download is actually complete
+const MAX_PROGRESS_BEFORE_COMPLETION = 99;
+
+// Estimate the total download size in bytes for a job. If the media source
+// provides a Size, use that. Otherwise, if we have a bitrate and run time
+// (RunTimeTicks), approximate size = (bitrate bits/sec * seconds) / 8.
const calculateEstimatedSize = (p: JobStatus): number => {
- let size = p.mediaSource.Size;
- const maxBitrate = p.maxBitrate.value;
- if (
- maxBitrate &&
- size &&
- p.mediaSource.Bitrate &&
- maxBitrate < p.mediaSource.Bitrate
- ) {
- size = (size / p.mediaSource.Bitrate) * maxBitrate;
- }
- // This function is for estimated size, so just return the adjusted size
- return size ?? 0;
-};
+ const size = p.mediaSource?.Size || 0;
+ const maxBitrate = p.maxBitrate?.value;
+ const runTimeTicks = (p.item?.RunTimeTicks || 0) as number;
-// Helper to calculate download speed
-const calculateSpeed = (
- process: JobStatus,
- newBytesDownloaded: number,
-): number | undefined => {
- const { bytesDownloaded: oldBytes = 0, lastProgressUpdateTime } = process;
- const deltaBytes = newBytesDownloaded - oldBytes;
-
- if (lastProgressUpdateTime && deltaBytes > 0) {
- const deltaTimeInSeconds =
- (Date.now() - new Date(lastProgressUpdateTime).getTime()) / 1000;
- if (deltaTimeInSeconds > 0) {
- return deltaBytes / deltaTimeInSeconds;
+ if (!size && maxBitrate && runTimeTicks > 0) {
+ // Jellyfin RunTimeTicks are in 10,000,000 ticks per second
+ const seconds = runTimeTicks / 10000000;
+ if (seconds > 0) {
+ // maxBitrate is in bits per second; convert to bytes
+ return Math.round((maxBitrate / 8) * seconds);
}
}
- return undefined;
+
+ return size || 0;
+};
+
+// Calculate download speed in bytes/sec based on a job's last update time
+// and previously recorded bytesDownloaded.
+const calculateSpeed = (
+ p: JobStatus,
+ currentBytesDownloaded?: number,
+): number | undefined => {
+ // Prefer session-only deltas when available: lastSessionBytes + lastSessionUpdateTime
+ const now = Date.now();
+
+ if (p.lastSessionUpdateTime && p.lastSessionBytes !== undefined) {
+ const last = new Date(p.lastSessionUpdateTime).getTime();
+ const deltaTime = (now - last) / 1000;
+ if (deltaTime > 0) {
+ const current =
+ currentBytesDownloaded ?? p.bytesDownloaded ?? p.lastSessionBytes;
+ const deltaBytes = current - p.lastSessionBytes;
+ if (deltaBytes > 0) return deltaBytes / deltaTime;
+ }
+ }
+
+ // Fallback to total-based deltas for compatibility
+ if (!p.lastProgressUpdateTime || p.bytesDownloaded === undefined)
+ return undefined;
+ const last = new Date(p.lastProgressUpdateTime).getTime();
+ const deltaTime = (now - last) / 1000;
+ if (deltaTime <= 0) return undefined;
+ const prev = p.bytesDownloaded || 0;
+ const current = currentBytesDownloaded ?? prev;
+ const deltaBytes = current - prev;
+ if (deltaBytes <= 0) return undefined;
+ return deltaBytes / deltaTime;
};
export const processesAtom = atom([]);
@@ -170,27 +193,96 @@ function useDownloadProvider() {
const currentProcesses = [...processes, ...missingProcesses];
const updatedProcesses = currentProcesses.map((p) => {
- // fallback. Doesn't really work for transcodes as they may be a lot smaller.
- // We make an wild guess by comparing bitrates
+ // Enhanced filtering to prevent iOS zombie task interference
+ // Only update progress for downloads that are actively downloading
+ if (p.status !== "downloading") {
+ return p;
+ }
+
+ // Find task for this process
const task = tasks.find((s: any) => s.id === p.id);
+ if (!task) {
+ return p; // No task found, keep current state
+ }
+
+ /*
+ // TODO: Uncomment this block to re-enable iOS zombie task detection
+ // iOS: Extra validation to prevent zombie task interference
+ if (Platform.OS === "ios") {
+ // Check if we have multiple tasks for same ID (zombie detection)
+ const tasksForId = tasks.filter((t: any) => t.id === p.id);
+ if (tasksForId.length > 1) {
+ console.warn(
+ `[UPDATE] Detected ${tasksForId.length} zombie tasks for ${p.id}, ignoring progress update`,
+ );
+ return p; // Don't update progress from potentially conflicting tasks
+ }
+
+ // If task state looks suspicious (e.g., iOS task stuck in background), be conservative
+ if (
+ task.state &&
+ ["SUSPENDED", "PAUSED"].includes(task.state) &&
+ p.status === "downloading"
+ ) {
+ console.warn(
+ `[UPDATE] Task ${p.id} has suspicious state ${task.state}, ignoring progress update`,
+ );
+ return p;
+ }
+ }
+ */
+
if (task && p.status === "downloading") {
const estimatedSize = calculateEstimatedSize(p);
let progress = p.progress;
- if (estimatedSize > 0) {
- progress = (100 / estimatedSize) * task.bytesDownloaded;
+
+ // If we have a pausedProgress snapshot then merge current session
+ // progress into it. We accept pausedProgress === 0 as valid because
+ // users can pause immediately after starting.
+ if (p.pausedProgress !== undefined) {
+ const totalBytesDownloaded =
+ (p.pausedBytes ?? 0) + task.bytesDownloaded;
+
+ // Calculate progress based on total bytes downloaded vs estimated size
+ progress =
+ estimatedSize > 0
+ ? (totalBytesDownloaded / estimatedSize) * 100
+ : 0;
+
+ // Use the total accounted bytes when computing speed so the
+ // displayed speed and progress remain consistent after resume.
+ const speed = calculateSpeed(p, totalBytesDownloaded);
+
+ return {
+ ...p,
+ progress: Math.min(progress, MAX_PROGRESS_BEFORE_COMPLETION),
+ speed,
+ bytesDownloaded: totalBytesDownloaded,
+ lastProgressUpdateTime: new Date(),
+ estimatedTotalSizeBytes: estimatedSize,
+ // Set session bytes to total bytes downloaded
+ lastSessionBytes: totalBytesDownloaded,
+ lastSessionUpdateTime: new Date(),
+ };
+ } else {
+ if (estimatedSize > 0) {
+ progress = (100 / estimatedSize) * task.bytesDownloaded;
+ }
+ if (progress >= 100) {
+ progress = MAX_PROGRESS_BEFORE_COMPLETION;
+ }
+ const speed = calculateSpeed(p, task.bytesDownloaded);
+ return {
+ ...p,
+ progress,
+ speed,
+ bytesDownloaded: task.bytesDownloaded,
+ lastProgressUpdateTime: new Date(),
+ estimatedTotalSizeBytes: estimatedSize,
+ lastSessionBytes: task.bytesDownloaded,
+ lastSessionUpdateTime: new Date(),
+ };
}
- if (progress >= 100) {
- progress = 99;
- }
- const speed = calculateSpeed(p, task.bytesDownloaded);
- return {
- ...p,
- progress,
- speed,
- bytesDownloaded: task.bytesDownloaded,
- lastProgressUpdateTime: new Date(),
- estimatedTotalSizeBytes: estimatedSize,
- };
}
return p;
});
@@ -372,10 +464,76 @@ function useDownloadProvider() {
async (process: JobStatus) => {
if (!process?.item.Id || !authHeader) throw new Error("No item id");
+ // Enhanced cleanup for existing tasks to prevent duplicates
+ try {
+ const allTasks = await BackGroundDownloader.checkForExistingDownloads();
+ const existingTasks = allTasks?.filter((t: any) => t.id === process.id);
+
+ if (existingTasks && existingTasks.length > 0) {
+ console.log(
+ `[START] Found ${existingTasks.length} existing task(s) for ${process.id}, cleaning up...`,
+ );
+
+ for (let i = 0; i < existingTasks.length; i++) {
+ const existingTask = existingTasks[i];
+ console.log(
+ `[START] Cleaning up task ${i + 1}/${existingTasks.length} for ${process.id}`,
+ );
+
+ try {
+ /*
+ // TODO: Uncomment this block to re-enable iOS-specific cleanup
+ // iOS: More aggressive cleanup sequence
+ if (Platform.OS === "ios") {
+ try {
+ await existingTask.pause();
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ } catch (_pauseErr) {
+ // Ignore pause errors
+ }
+
+ await existingTask.stop();
+ await new Promise((resolve) => setTimeout(resolve, 50));
+
+ // Multiple complete handler calls to ensure cleanup
+ BackGroundDownloader.completeHandler(process.id);
+ await new Promise((resolve) => setTimeout(resolve, 25));
+ } else {
+ */
+
+ // Simple cleanup for all platforms (currently Android only)
+ await existingTask.stop();
+ BackGroundDownloader.completeHandler(process.id);
+
+ /* } // End of iOS block - uncomment when re-enabling iOS functionality */
+
+ console.log(
+ `[START] Successfully cleaned up task ${i + 1} for ${process.id}`,
+ );
+ } catch (taskError) {
+ console.warn(
+ `[START] Failed to cleanup task ${i + 1} for ${process.id}:`,
+ taskError,
+ );
+ }
+ }
+
+ // Cleanup delay (simplified for Android)
+ const cleanupDelay = 200; // Platform.OS === "ios" ? 500 : 200;
+ await new Promise((resolve) => setTimeout(resolve, cleanupDelay));
+ console.log(`[START] Cleanup completed for ${process.id}`);
+ }
+ } catch (error) {
+ console.warn(
+ `[START] Failed to check/cleanup existing tasks for ${process.id}:`,
+ error,
+ );
+ }
+
updateProcess(process.id, {
speed: undefined,
status: "downloading",
- progress: 0,
+ progress: process.progress || 0, // Preserve existing progress for resume
});
BackGroundDownloader?.setConfig({
@@ -396,21 +554,42 @@ function useDownloadProvider() {
.begin(() => {
updateProcess(process.id, {
status: "downloading",
- progress: 0,
- bytesDownloaded: 0,
+ progress: process.progress || 0,
+ bytesDownloaded: process.bytesDownloaded || 0,
lastProgressUpdateTime: new Date(),
+ lastSessionBytes: process.lastSessionBytes || 0,
+ lastSessionUpdateTime: new Date(),
});
})
.progress(
throttle((data) => {
updateProcess(process.id, (currentProcess) => {
- const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
+ // If this is a resumed download, add the paused bytes to current session bytes
+ const resumedBytes = currentProcess.pausedBytes || 0;
+ const totalBytes = data.bytesDownloaded + resumedBytes;
+
+ // Calculate progress based on total bytes if we have resumed bytes
+ let percent: number;
+ if (resumedBytes > 0 && data.bytesTotal > 0) {
+ // For resumed downloads, calculate based on estimated total size
+ const estimatedTotal =
+ currentProcess.estimatedTotalSizeBytes ||
+ data.bytesTotal + resumedBytes;
+ percent = (totalBytes / estimatedTotal) * 100;
+ } else {
+ // For fresh downloads, use normal calculation
+ percent = (data.bytesDownloaded / data.bytesTotal) * 100;
+ }
+
return {
- speed: calculateSpeed(currentProcess, data.bytesDownloaded),
+ speed: calculateSpeed(currentProcess, totalBytes),
status: "downloading",
- progress: percent,
- bytesDownloaded: data.bytesDownloaded,
+ progress: Math.min(percent, MAX_PROGRESS_BEFORE_COMPLETION),
+ bytesDownloaded: totalBytes,
lastProgressUpdateTime: new Date(),
+ // update session-only counters - use current session bytes only for speed calc
+ lastSessionBytes: data.bytesDownloaded,
+ lastSessionUpdateTime: new Date(),
};
});
}, 500),
@@ -542,7 +721,17 @@ function useDownloadProvider() {
if (activeDownloads < concurrentLimit) {
const queuedDownload = processes.find((p) => p.status === "queued");
if (queuedDownload) {
- startDownload(queuedDownload);
+ // Reserve the slot immediately to avoid race where startDownload's
+ // asynchronous begin callback hasn't executed yet and multiple
+ // downloads are started, bypassing the concurrent limit.
+ updateProcess(queuedDownload.id, { status: "downloading" });
+ startDownload(queuedDownload).catch((error) => {
+ console.error("Failed to start download:", error);
+ updateProcess(queuedDownload.id, { status: "error" });
+ toast.error(t("home.downloads.toasts.failed_to_start_download"), {
+ description: error.message || "Unknown error",
+ });
+ });
}
}
}, [processes, settings?.remuxConcurrentLimit, startDownload]);
@@ -551,8 +740,38 @@ function useDownloadProvider() {
async (id: string) => {
const tasks = await BackGroundDownloader.checkForExistingDownloads();
const task = tasks?.find((t: any) => t.id === id);
- task?.stop();
- BackGroundDownloader.completeHandler(id);
+ if (task) {
+ // On iOS, suspended tasks need to be cancelled properly
+ if (Platform.OS === "ios") {
+ const state = task.state || task.state?.();
+ if (
+ state === "PAUSED" ||
+ state === "paused" ||
+ state === "SUSPENDED" ||
+ state === "suspended"
+ ) {
+ // For suspended tasks, we need to resume first, then stop
+ try {
+ await task.resume();
+ // Small delay to allow resume to take effect
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ } catch (_resumeError) {
+ // Resume might fail, continue with stop
+ }
+ }
+ }
+
+ try {
+ task.stop();
+ } catch (_err) {
+ // ignore stop errors
+ }
+ try {
+ BackGroundDownloader.completeHandler(id);
+ } catch (_err) {
+ // ignore
+ }
+ }
setProcesses((prev) => prev.filter((process) => process.id !== id));
manageDownloadQueue();
},
@@ -575,7 +794,7 @@ function useDownloadProvider() {
intermediates: true,
});
} catch (_error) {
- toast.error(t("Failed to clean cache directory."));
+ toast.error(t("home.downloads.toasts.failed_to_clean_cache_directory"));
}
};
@@ -611,9 +830,13 @@ function useDownloadProvider() {
status: "queued",
timestamp: new Date(),
};
- setProcesses((prev) => [...prev, job]);
+ setProcesses((prev) => {
+ // Remove any existing processes for this item to prevent duplicates
+ const filtered = prev.filter((p) => p.id !== item.Id);
+ return [...filtered, job];
+ });
toast.success(
- t("home.downloads.toasts.download_stated_for_item", {
+ t("home.downloads.toasts.download_started_for_item", {
item: item.Name,
}),
{
@@ -791,12 +1014,99 @@ function useDownloadProvider() {
const process = processes.find((p) => p.id === id);
if (!process) throw new Error("No active download");
+ // TODO: iOS pause functionality temporarily disabled due to background task issues
+ // Remove this check to re-enable iOS pause functionality in the future
+ if (Platform.OS === "ios") {
+ console.warn(
+ `[PAUSE] Pause functionality temporarily disabled on iOS for ${id}`,
+ );
+ throw new Error("Pause functionality is currently disabled on iOS");
+ }
+
const tasks = await BackGroundDownloader.checkForExistingDownloads();
const task = tasks?.find((t: any) => t.id === id);
if (!task) throw new Error("No task found");
- task.pause();
- updateProcess(id, { status: "paused" });
+ // Get current progress before stopping
+ const currentProgress = process.progress;
+ const currentBytes = process.bytesDownloaded || task.bytesDownloaded || 0;
+
+ console.log(
+ `[PAUSE] Starting pause for ${id}. Current bytes: ${currentBytes}, Progress: ${currentProgress}%`,
+ );
+
+ try {
+ /*
+ // TODO: Uncomment this block to re-enable iOS pause functionality
+ // iOS-specific aggressive cleanup approach based on GitHub issue #26
+ if (Platform.OS === "ios") {
+ // Get ALL tasks for this ID - there might be multiple zombie tasks
+ const allTasks =
+ await BackGroundDownloader.checkForExistingDownloads();
+ const tasksForId = allTasks?.filter((t: any) => t.id === id) || [];
+
+ console.log(`[PAUSE] Found ${tasksForId.length} task(s) for ${id}`);
+
+ // Stop ALL tasks for this ID to prevent zombie processes
+ for (let i = 0; i < tasksForId.length; i++) {
+ const taskToStop = tasksForId[i];
+ console.log(
+ `[PAUSE] Stopping task ${i + 1}/${tasksForId.length} for ${id}`,
+ );
+
+ try {
+ // iOS: pause โ stop sequence with delays (based on issue research)
+ await taskToStop.pause();
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ await taskToStop.stop();
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ console.log(
+ `[PAUSE] Successfully stopped task ${i + 1} for ${id}`,
+ );
+ } catch (taskError) {
+ console.warn(
+ `[PAUSE] Failed to stop task ${i + 1} for ${id}:`,
+ taskError,
+ );
+ }
+ }
+
+ // Extra cleanup delay for iOS NSURLSession to fully stop
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ } else {
+ */
+
+ // Android: simpler approach (currently the only active platform)
+ await task.stop();
+
+ /* } // End of iOS block - uncomment when re-enabling iOS functionality */
+
+ // Clean up the native task handler
+ try {
+ BackGroundDownloader.completeHandler(id);
+ } catch (_err) {
+ console.warn(`[PAUSE] Handler cleanup warning for ${id}:`, _err);
+ }
+
+ // Update process state to paused
+ updateProcess(id, {
+ status: "paused",
+ progress: currentProgress,
+ bytesDownloaded: currentBytes,
+ pausedAt: new Date(),
+ pausedProgress: currentProgress,
+ pausedBytes: currentBytes,
+ lastSessionBytes: process.lastSessionBytes ?? currentBytes,
+ lastSessionUpdateTime: process.lastSessionUpdateTime ?? new Date(),
+ });
+
+ console.log(`Download paused successfully: ${id}`);
+ } catch (error) {
+ console.error("Error pausing task:", error);
+ throw error;
+ }
},
[processes, updateProcess],
);
@@ -806,38 +1116,79 @@ function useDownloadProvider() {
const process = processes.find((p) => p.id === id);
if (!process) throw new Error("No active download");
- const tasks = await BackGroundDownloader.checkForExistingDownloads();
- const task = tasks?.find((t: any) => t.id === id);
- if (!task) throw new Error("No task found");
-
- // Check if task state allows resuming
- if (task.state === "FAILED") {
+ // TODO: iOS resume functionality temporarily disabled due to background task issues
+ // Remove this check to re-enable iOS resume functionality in the future
+ if (Platform.OS === "ios") {
console.warn(
- "Download task failed, cannot resume. Restarting download.",
+ `[RESUME] Resume functionality temporarily disabled on iOS for ${id}`,
);
- // For failed tasks, we need to restart rather than resume
- await startDownload(process);
- return;
+ throw new Error("Resume functionality is currently disabled on iOS");
}
- try {
- task.resume();
- updateProcess(id, { status: "downloading" });
- } catch (error: any) {
- // Handle specific ERROR_CANNOT_RESUME error
- if (
- error?.error === "ERROR_CANNOT_RESUME" ||
- error?.errorCode === 1008
- ) {
- console.warn("Cannot resume download, attempting to restart instead");
- await startDownload(process);
- return; // Return early to prevent error from bubbling up
- } else {
- // Only log error for non-handled cases
- console.error("Error resuming download:", error);
- throw error; // Re-throw other errors
+ console.log(
+ `[RESUME] Attempting to resume ${id}. Paused bytes: ${process.pausedBytes}, Progress: ${process.pausedProgress}%`,
+ );
+
+ /*
+ // TODO: Uncomment this block to re-enable iOS resume functionality
+ // Enhanced cleanup for iOS based on GitHub issue research
+ if (Platform.OS === "ios") {
+ try {
+ // Clean up any lingering zombie tasks first (critical for iOS)
+ const allTasks =
+ await BackGroundDownloader.checkForExistingDownloads();
+ const existingTasks = allTasks?.filter((t: any) => t.id === id) || [];
+
+ if (existingTasks.length > 0) {
+ console.log(
+ `[RESUME] Found ${existingTasks.length} lingering task(s), cleaning up...`,
+ );
+
+ for (const task of existingTasks) {
+ try {
+ await task.stop();
+ BackGroundDownloader.completeHandler(id);
+ } catch (cleanupError) {
+ console.warn(`[RESUME] Cleanup error:`, cleanupError);
+ }
+ }
+
+ // Wait for iOS cleanup to complete
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ }
+ } catch (error) {
+ console.warn(`[RESUME] Pre-resume cleanup failed:`, error);
}
}
+ */
+
+ // Simple approach: always restart the download from where we left off
+ // This works consistently across all platforms (currently Android only)
+ if (
+ process.pausedProgress !== undefined &&
+ process.pausedBytes !== undefined
+ ) {
+ // We have saved pause state - restore it and restart
+ updateProcess(id, {
+ progress: process.pausedProgress,
+ bytesDownloaded: process.pausedBytes,
+ status: "downloading",
+ // Reset session counters for proper speed calculation
+ lastSessionBytes: process.pausedBytes,
+ lastSessionUpdateTime: new Date(),
+ });
+
+ // Small delay to ensure any cleanup in startDownload completes
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ const updatedProcess = processes.find((p) => p.id === id);
+ await startDownload(updatedProcess || process);
+
+ console.log(`Download resumed successfully: ${id}`);
+ } else {
+ // No pause state - start from beginning
+ await startDownload(process);
+ }
},
[processes, updateProcess, startDownload],
);
@@ -861,6 +1212,21 @@ function useDownloadProvider() {
cleanCacheDirectory,
updateDownloadedItem,
appSizeUsage,
+ dumpDownloadDiagnostics: async (id?: string) => {
+ // Collect JS-side processes and native task info (best-effort)
+ const tasks = BackGroundDownloader
+ ? await BackGroundDownloader.checkForExistingDownloads()
+ : [];
+ const extra: any = {
+ processes,
+ nativeTasks: tasks || [],
+ };
+ if (id) {
+ const p = processes.find((x) => x.id === id);
+ extra.focusedProcess = p || null;
+ }
+ return dumpDownloadDiagnostics(extra);
+ },
};
}
diff --git a/providers/Downloads/types.ts b/providers/Downloads/types.ts
index ee74b25d..cff87ddf 100644
--- a/providers/Downloads/types.ts
+++ b/providers/Downloads/types.ts
@@ -129,4 +129,14 @@ export type JobStatus = {
/** Estimated total size of the download in bytes (optional) this is used when we
* download transcoded content because we don't know the size of the file until it's downloaded */
estimatedTotalSizeBytes?: number;
+ /** Timestamp when the download was paused (optional) */
+ pausedAt?: Date;
+ /** Progress percentage when download was paused (optional) */
+ pausedProgress?: number;
+ /** Bytes downloaded when download was paused (optional) */
+ pausedBytes?: number;
+ /** Bytes downloaded in the current session (since last resume). Used for session-only speed calculation. */
+ lastSessionBytes?: number;
+ /** Timestamp when the session-only bytes were last updated. */
+ lastSessionUpdateTime?: Date;
};
diff --git a/translations/en.json b/translations/en.json
index 834fd183..8578567a 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -223,7 +223,9 @@
"system": "System"
},
"toasts": {
- "error_deleting_files": "Error Deleting Files"
+ "error_deleting_files": "Error Deleting Files",
+ "background_downloads_enabled": "Background downloads enabled",
+ "background_downloads_disabled": "Background downloads disabled"
}
},
"sessions": {
@@ -266,11 +268,23 @@
"download_completed": "Download Completed",
"download_failed_for_item": "Download failed for {{item}} - {{error}}",
"download_completed_for_item": "Download Completed for {{item}}",
+ "download_started_for_item": "Download Started for {{item}}",
+ "failed_to_start_download": "Failed to start download",
"all_files_folders_and_jobs_deleted_successfully": "All files, folders, and jobs deleted successfully",
+ "failed_to_clean_cache_directory": "Failed to clean cache directory",
+ "could_not_get_download_url_for_item": "Could not get download URL for {{itemName}}",
"go_to_downloads": "Go to Downloads"
}
}
},
+ "common": {
+ "select": "Select",
+ "no_trailer_available": "No trailer available",
+ "video": "Video",
+ "audio": "Audio",
+ "subtitle": "Subtitle",
+ "play": "Play"
+ },
"search": {
"search": "Search...",
"x_items": "{{count}} Items",
diff --git a/utils/log.tsx b/utils/log.tsx
index 88ca475e..956f1fb0 100644
--- a/utils/log.tsx
+++ b/utils/log.tsx
@@ -77,6 +77,17 @@ export const clearLogs = () => {
storage.delete("logs");
};
+export const dumpDownloadDiagnostics = (extra: any = {}) => {
+ const diagnostics = {
+ timestamp: new Date().toISOString(),
+ processes: extra?.processes || [],
+ nativeTasks: extra?.nativeTasks || [],
+ focusedProcess: extra?.focusedProcess || null,
+ };
+ writeDebugLog("Download diagnostics", diagnostics);
+ return diagnostics;
+};
+
export function useLog() {
const context = useContext(LogContext);
if (context === null) {
From 0ec44add7d9513191e979a6277cc0ddcfcfb6e96 Mon Sep 17 00:00:00 2001
From: Fredrik Burmester
Date: Mon, 29 Sep 2025 15:02:24 +0200
Subject: [PATCH 11/27] fix: navigatte to item
---
components/AppleTVCarousel.tsx | 99 +++++++++++++++++++++-------------
1 file changed, 63 insertions(+), 36 deletions(-)
diff --git a/components/AppleTVCarousel.tsx b/components/AppleTVCarousel.tsx
index 2f5b2d50..c439069d 100644
--- a/components/AppleTVCarousel.tsx
+++ b/components/AppleTVCarousel.tsx
@@ -7,9 +7,10 @@ import {
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
+import { useRouter } from "expo-router";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react";
-import { Dimensions, Pressable, View } from "react-native";
+import { Dimensions, Pressable, TouchableOpacity, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
Easing,
@@ -25,6 +26,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { ItemImage } from "./common/ItemImage";
+import { getItemNavigation } from "./common/TouchableItemRouter";
import type { SelectedOptions } from "./ItemContent";
import { PlayButton } from "./PlayButton";
import { PlayedStatus } from "./PlayedStatus";
@@ -150,6 +152,7 @@ export const AppleTVCarousel: React.FC = ({
const api = useAtomValue(apiAtom);
const user = useAtomValue(userAtom);
const { isConnected, serverConnected } = useNetworkStatus();
+ const router = useRouter();
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const translateX = useSharedValue(-currentIndex * screenWidth);
@@ -301,6 +304,14 @@ export const AppleTVCarousel: React.FC = ({
[hasItems, items, onItemChange, translateX],
);
+ const navigateToItem = useCallback(
+ (item: BaseItemDto) => {
+ const navigation = getItemNavigation(item, "(home)");
+ router.push(navigation as any);
+ },
+ [router],
+ );
+
const panGesture = Gesture.Pan()
.activeOffsetX([-PAN_ACTIVE_OFFSET, PAN_ACTIVE_OFFSET])
.onUpdate((event) => {
@@ -591,7 +602,8 @@ export const AppleTVCarousel: React.FC = ({
{/* Logo Section */}
{itemLogoUrl && (
- navigateToItem(item)}
style={{
position: "absolute",
bottom: LOGO_BOTTOM_POSITION,
@@ -611,7 +623,7 @@ export const AppleTVCarousel: React.FC = ({
}}
contentFit='contain'
/>
-
+
)}
{/* Type and Genres Section */}
@@ -625,41 +637,56 @@ export const AppleTVCarousel: React.FC = ({
alignItems: "center",
}}
>
-
- {(() => {
- const typeLabel =
- item.Type === "Series"
- ? "TV Show"
- : item.Type === "Movie"
- ? "Movie"
- : item.Type || "";
+ navigateToItem(item)}>
+
+ {(() => {
+ let typeLabel = "";
- const genres =
- item.Genres && item.Genres.length > 0
- ? item.Genres.slice(0, MAX_GENRES_COUNT).join(" โข ")
- : "";
+ if (item.Type === "Episode") {
+ // For episodes, show season and episode number
+ const season = item.ParentIndexNumber;
+ const episode = item.IndexNumber;
+ if (season && episode) {
+ typeLabel = `S${season} โข E${episode}`;
+ } else {
+ typeLabel = "Episode";
+ }
+ } else {
+ typeLabel =
+ item.Type === "Series"
+ ? "TV Show"
+ : item.Type === "Movie"
+ ? "Movie"
+ : item.Type || "";
+ }
- if (typeLabel && genres) {
- return `${typeLabel} โข ${genres}`;
- } else if (typeLabel) {
- return typeLabel;
- } else if (genres) {
- return genres;
- } else {
- return "";
- }
- })()}
-
+ const genres =
+ item.Genres && item.Genres.length > 0
+ ? item.Genres.slice(0, MAX_GENRES_COUNT).join(" โข ")
+ : "";
+
+ if (typeLabel && genres) {
+ return `${typeLabel} โข ${genres}`;
+ } else if (typeLabel) {
+ return typeLabel;
+ } else if (genres) {
+ return genres;
+ } else {
+ return "";
+ }
+ })()}
+
+
{/* Controls Section */}
From f104e952ab0b7ffc45483065e5ab014b53740285 Mon Sep 17 00:00:00 2001
From: Gauvain <68083474+Gauvino@users.noreply.github.com>
Date: Mon, 29 Sep 2025 18:37:02 +0200
Subject: [PATCH 12/27] feat: add automated PR comments for build artifacts
(#1099)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.github/workflows/artifact-comment.yml | 124 +++++++++++++++++++++++++
1 file changed, 124 insertions(+)
create mode 100644 .github/workflows/artifact-comment.yml
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
new file mode 100644
index 00000000..112f7113
--- /dev/null
+++ b/.github/workflows/artifact-comment.yml
@@ -0,0 +1,124 @@
+name: ๐ Artifact Comment on PR
+
+concurrency:
+ group: artifact-comment-${{ github.event.workflow_run.head_branch }}
+ cancel-in-progress: true
+
+on:
+ workflow_run:
+ workflows: ["๐ค iOS IPA Build (Phone + TV)", "๐ค Android APK Build (Phone + TV)"]
+ types:
+ - completed
+
+jobs:
+ comment-artifacts:
+ if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+ actions: read
+
+ steps:
+ - name: ๐ Get PR and Artifacts
+ uses: actions/github-script@v8
+ with:
+ script: |
+ // Find PR associated with this commit
+ const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ commit_sha: "${{ github.event.workflow_run.head_sha }}"
+ });
+
+ if (pullRequests.length === 0) {
+ core.setFailed('No pull request found for this commit');
+ return;
+ }
+
+ const pr = pullRequests[0];
+ const runId = "${{ github.event.workflow_run.id }}";
+
+ // Get artifacts from the workflow run
+ const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: runId
+ });
+
+ if (artifacts.artifacts.length === 0) {
+ console.log('No artifacts found for this run');
+ return;
+ }
+
+ // Sort and categorize artifacts
+ const androidArtifacts = artifacts.artifacts
+ .filter(a => a.name.includes('android'))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ const iosArtifacts = artifacts.artifacts
+ .filter(a => a.name.includes('ios'))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+ // Build comment body with table format
+ let commentBody = `## ๐ฑ Build Artifacts Ready!\n\n`;
+ commentBody += `โ
**Workflow completed successfully** for PR #${pr.number}\n`;
+ commentBody += `๐ฆ **${artifacts.artifacts.length} artifacts** generated from commit [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`;
+
+ // Create table for better organization
+ commentBody += `| Platform | Device Type | Download Link |\n`;
+ commentBody += `|----------|-------------|---------------|\n`;
+
+ // Add Android artifacts
+ androidArtifacts.forEach(artifact => {
+ const isTV = artifact.name.includes('tv');
+ const deviceType = isTV ? '๐บ Android TV' : '๐ฑ Android Phone';
+ const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
+ commentBody += `| ๐ค Android | ${deviceType} | [๐ฅ Download APK](${nightlyLink}) |\n`;
+ });
+
+ // Add iOS artifacts
+ iosArtifacts.forEach(artifact => {
+ const isTV = artifact.name.includes('tv');
+ const deviceType = isTV ? '๐บ Apple TV' : '๐ฑ iPhone/iPad';
+ const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
+ commentBody += `| ๐ iOS | ${deviceType} | [๐ฅ Download IPA](${nightlyLink}) |\n`;
+ });
+
+ commentBody += `\n`;
+ commentBody += `### ๐ง Installation Instructions\n\n`;
+ commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`;
+ commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`;
+ commentBody += `> โ ๏ธ **Note**: Artifacts expire in 7 days from build date\n\n`;
+ commentBody += `*Auto-generated by [GitHub Actions](${context.payload.workflow_run.html_url})*`;
+
+ // Find existing bot comment to update
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number
+ });
+
+ const botComment = comments.find(comment =>
+ comment.user.type === 'Bot' &&
+ comment.body.includes('Build Artifacts Ready!')
+ );
+
+ if (botComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: commentBody
+ });
+ console.log(`โ
Updated comment ${botComment.id} on PR #${pr.number}`);
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ body: commentBody
+ });
+ console.log(`โ
Created new comment on PR #${pr.number}`);
+ }
From 7fe3ca84842c292b612e7902fe7c6ab0ea06a6d1 Mon Sep 17 00:00:00 2001
From: Gauvain <68083474+Gauvino@users.noreply.github.com>
Date: Mon, 29 Sep 2025 22:31:41 +0200
Subject: [PATCH 13/27] feat: add automated PR comments for build artifacts
(#1100)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.github/workflows/artifact-comment.yml | 103 +++++++++++++++----------
1 file changed, 61 insertions(+), 42 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 112f7113..67e31268 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -1,12 +1,17 @@
name: ๐ Artifact Comment on PR
concurrency:
- group: artifact-comment-${{ github.event.workflow_run.head_branch }}
- cancel-in-progress: true
+ group: artifact-comment-${{ github.event.workflow_run.id }}
+ cancel-in-progress: false
on:
+ workflow_dispatch:
workflow_run:
- workflows: ["๐ค iOS IPA Build (Phone + TV)", "๐ค Android APK Build (Phone + TV)"]
+ workflows:
+ - "๐๏ธ Build iOS IPA (phone)"
+ - "๐๏ธ Build iOS IPA (tv)"
+ - "๐๏ธ Build Android APK (phone)"
+ - "๐๏ธ Build Android APK (tv)"
types:
- completed
@@ -24,33 +29,39 @@ jobs:
uses: actions/github-script@v8
with:
script: |
+ console.log('Workflow run event:', JSON.stringify(github.event.workflow_run, null, 2));
+
// Find PR associated with this commit
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: "${{ github.event.workflow_run.head_sha }}"
});
-
+
if (pullRequests.length === 0) {
- core.setFailed('No pull request found for this commit');
+ console.log('No pull request found for commit: ${{ github.event.workflow_run.head_sha }}');
return;
}
-
+
const pr = pullRequests[0];
const runId = "${{ github.event.workflow_run.id }}";
-
+
+ console.log(`Found PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`);
+
// Get artifacts from the workflow run
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId
});
-
- if (artifacts.artifacts.length === 0) {
+
+ console.log(`Found ${artifacts?.artifacts?.length || 0} artifacts`);
+
+ if (!artifacts || artifacts.artifacts.length === 0) {
console.log('No artifacts found for this run');
return;
}
-
+
// Sort and categorize artifacts
const androidArtifacts = artifacts.artifacts
.filter(a => a.name.includes('android'))
@@ -58,51 +69,59 @@ jobs:
const iosArtifacts = artifacts.artifacts
.filter(a => a.name.includes('ios'))
.sort((a, b) => a.name.localeCompare(b.name));
-
+
+ console.log(`Android artifacts: ${androidArtifacts.length}, iOS artifacts: ${iosArtifacts.length}`);
+
// Build comment body with table format
let commentBody = `## ๐ฑ Build Artifacts Ready!\n\n`;
commentBody += `โ
**Workflow completed successfully** for PR #${pr.number}\n`;
commentBody += `๐ฆ **${artifacts.artifacts.length} artifacts** generated from commit [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`;
-
- // Create table for better organization
- commentBody += `| Platform | Device Type | Download Link |\n`;
- commentBody += `|----------|-------------|---------------|\n`;
-
- // Add Android artifacts
- androidArtifacts.forEach(artifact => {
- const isTV = artifact.name.includes('tv');
- const deviceType = isTV ? '๐บ Android TV' : '๐ฑ Android Phone';
- const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
- commentBody += `| ๐ค Android | ${deviceType} | [๐ฅ Download APK](${nightlyLink}) |\n`;
- });
-
- // Add iOS artifacts
- iosArtifacts.forEach(artifact => {
- const isTV = artifact.name.includes('tv');
- const deviceType = isTV ? '๐บ Apple TV' : '๐ฑ iPhone/iPad';
- const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
- commentBody += `| ๐ iOS | ${deviceType} | [๐ฅ Download IPA](${nightlyLink}) |\n`;
- });
-
- commentBody += `\n`;
- commentBody += `### ๐ง Installation Instructions\n\n`;
- commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`;
- commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`;
- commentBody += `> โ ๏ธ **Note**: Artifacts expire in 7 days from build date\n\n`;
- commentBody += `*Auto-generated by [GitHub Actions](${context.payload.workflow_run.html_url})*`;
-
+
+ if (androidArtifacts.length === 0 && iosArtifacts.length === 0) {
+ commentBody += `โ ๏ธ No mobile app artifacts found in this build.\n\n`;
+ } else {
+ // Create table for better organization
+ commentBody += `| Platform | Device Type | Download Link |\n`;
+ commentBody += `|----------|-------------|---------------|\n`;
+
+ // Add Android artifacts
+ androidArtifacts.forEach(artifact => {
+ const isTV = artifact.name.includes('tv');
+ const deviceType = isTV ? '๐บ Android TV' : '๐ฑ Android Phone';
+ const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
+ commentBody += `| ๐ค Android | ${deviceType} | [๐ฅ Download APK](${nightlyLink}) |\n`;
+ });
+
+ // Add iOS artifacts
+ iosArtifacts.forEach(artifact => {
+ const isTV = artifact.name.includes('tv');
+ const deviceType = isTV ? '๐บ Apple TV' : '๐ฑ iPhone/iPad';
+ const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
+ commentBody += `| ๐ iOS | ${deviceType} | [๐ฅ Download IPA](${nightlyLink}) |\n`;
+ });
+
+ commentBody += `\n`;
+ commentBody += `### ๐ง Installation Instructions\n\n`;
+ commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`;
+ commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`;
+ commentBody += `> โ ๏ธ **Note**: Artifacts expire in 7 days from build date\n\n`;
+ }
+
+ commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})*`;
+ commentBody += `\n`;
+
// Find existing bot comment to update
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});
-
+
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
- comment.body.includes('Build Artifacts Ready!')
+ comment.body.includes('')
);
-
+
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
From 92e40c7aa0d1ca4ddff4cbfce786f9c080dea2a9 Mon Sep 17 00:00:00 2001
From: Gauvain <68083474+Gauvino@users.noreply.github.com>
Date: Mon, 29 Sep 2025 23:16:12 +0200
Subject: [PATCH 14/27] feat: Build in pr (#1101)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.github/workflows/artifact-comment.yml | 75 +++++++++++++++++---------
1 file changed, 51 insertions(+), 24 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 67e31268..14e74800 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -1,23 +1,21 @@
name: ๐ Artifact Comment on PR
concurrency:
- group: artifact-comment-${{ github.event.workflow_run.id }}
+ group: artifact-comment-${{ github.event.workflow_run.id || github.run_id }}
cancel-in-progress: false
on:
- workflow_dispatch:
+ workflow_dispatch: # Allow manual testing
workflow_run:
workflows:
- - "๐๏ธ Build iOS IPA (phone)"
- - "๐๏ธ Build iOS IPA (tv)"
- - "๐๏ธ Build Android APK (phone)"
- - "๐๏ธ Build Android APK (tv)"
+ - "๐ค Android APK Build (Phone + TV)"
+ - "๐ค iOS IPA Build (Phone + TV)"
types:
- completed
jobs:
comment-artifacts:
- if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
+ if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
permissions:
contents: read
@@ -29,23 +27,56 @@ jobs:
uses: actions/github-script@v8
with:
script: |
- console.log('Workflow run event:', JSON.stringify(github.event.workflow_run, null, 2));
+ // Handle both workflow_run and manual dispatch events
+ let runId, pr;
- // Find PR associated with this commit
- const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
- owner: context.repo.owner,
- repo: context.repo.repo,
- commit_sha: "${{ github.event.workflow_run.head_sha }}"
- });
-
- if (pullRequests.length === 0) {
- console.log('No pull request found for commit: ${{ github.event.workflow_run.head_sha }}');
+ if (context.eventName === 'workflow_run') {
+ runId = github.event.workflow_run.id;
+
+ // Find PR associated with this commit
+ const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ commit_sha: github.event.workflow_run.head_sha
+ });
+
+ if (pullRequests.length === 0) {
+ console.log('No pull request found for commit:', github.event.workflow_run.head_sha);
+ return;
+ }
+ pr = pullRequests[0];
+
+ } else if (context.eventName === 'workflow_dispatch') {
+ // For manual testing, use most recent test workflow run
+ const { data: workflows } = await github.rest.actions.listWorkflowRuns({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ workflow_id: 'test-artifact.yml',
+ per_page: 1
+ });
+
+ if (workflows.workflow_runs.length === 0) {
+ console.log('No test workflow runs found');
+ return;
+ }
+
+ const testRun = workflows.workflow_runs[0];
+ runId = testRun.id;
+
+ // Get current PR for manual testing
+ const prNumber = context.payload.pull_request?.number || 1101;
+ const { data: prData } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: prNumber
+ });
+ pr = prData;
+
+ } else {
+ console.log('Unsupported event type:', context.eventName);
return;
}
- const pr = pullRequests[0];
- const runId = "${{ github.event.workflow_run.id }}";
-
console.log(`Found PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`);
// Get artifacts from the workflow run
@@ -55,8 +86,6 @@ jobs:
run_id: runId
});
- console.log(`Found ${artifacts?.artifacts?.length || 0} artifacts`);
-
if (!artifacts || artifacts.artifacts.length === 0) {
console.log('No artifacts found for this run');
return;
@@ -70,8 +99,6 @@ jobs:
.filter(a => a.name.includes('ios'))
.sort((a, b) => a.name.localeCompare(b.name));
- console.log(`Android artifacts: ${androidArtifacts.length}, iOS artifacts: ${iosArtifacts.length}`);
-
// Build comment body with table format
let commentBody = `## ๐ฑ Build Artifacts Ready!\n\n`;
commentBody += `โ
**Workflow completed successfully** for PR #${pr.number}\n`;
From 1ff09a2d34e7ac2e71c749deaef0ce8cf8d258fe Mon Sep 17 00:00:00 2001
From: Uruk
Date: Mon, 29 Sep 2025 23:51:03 +0200
Subject: [PATCH 15/27] feat: enhance artifact workflow to show real-time build
status
Improves the artifact comment workflow to provide better visibility into ongoing builds by:
- Triggering comments when builds start (requested event) instead of only on completion
- Using commit SHA for concurrency grouping to better handle multiple builds for the same commit
- Collecting artifacts from all recent build workflows for a PR rather than just the current run
- Adding a build status table showing progress of Android and iOS workflows
- Displaying progressive status updates even when builds are still in progress
- Enabling cancel-in-progress to prevent redundant workflow runs
This provides users with immediate feedback on build progress and comprehensive artifact availability across all platforms.
---
.github/workflows/artifact-comment.yml | 128 +++++++++++++++++--------
1 file changed, 87 insertions(+), 41 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 14e74800..60244409 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -1,8 +1,8 @@
name: ๐ Artifact Comment on PR
concurrency:
- group: artifact-comment-${{ github.event.workflow_run.id || github.run_id }}
- cancel-in-progress: false
+ group: artifact-comment-${{ github.event.workflow_run.head_sha || github.sha }}
+ cancel-in-progress: true
on:
workflow_dispatch: # Allow manual testing
@@ -12,10 +12,11 @@ on:
- "๐ค iOS IPA Build (Phone + TV)"
types:
- completed
+ - requested # Trigger when build starts
jobs:
comment-artifacts:
- if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success')
+ if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -28,11 +29,9 @@ jobs:
with:
script: |
// Handle both workflow_run and manual dispatch events
- let runId, pr;
+ let pr;
if (context.eventName === 'workflow_run') {
- runId = github.event.workflow_run.id;
-
// Find PR associated with this commit
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
@@ -47,22 +46,6 @@ jobs:
pr = pullRequests[0];
} else if (context.eventName === 'workflow_dispatch') {
- // For manual testing, use most recent test workflow run
- const { data: workflows } = await github.rest.actions.listWorkflowRuns({
- owner: context.repo.owner,
- repo: context.repo.repo,
- workflow_id: 'test-artifact.yml',
- per_page: 1
- });
-
- if (workflows.workflow_runs.length === 0) {
- console.log('No test workflow runs found');
- return;
- }
-
- const testRun = workflows.workflow_runs[0];
- runId = testRun.id;
-
// Get current PR for manual testing
const prNumber = context.payload.pull_request?.number || 1101;
const { data: prData } = await github.rest.pulls.get({
@@ -77,36 +60,97 @@ jobs:
return;
}
- console.log(`Found PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`);
+ console.log(`Processing PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`);
- // Get artifacts from the workflow run
+ // Get all recent workflow runs for this PR to collect artifacts from multiple builds
+ const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ head_sha: pr.head.sha,
+ per_page: 10
+ });
+
+ // Filter for build workflows only
+ const buildRuns = workflowRuns.workflow_runs.filter(run =>
+ run.name.includes('Android APK Build') ||
+ run.name.includes('iOS IPA Build')
+ );
+
+ console.log(`Found ${buildRuns.length} build workflow runs for this commit`);
+
+ // Collect artifacts from all completed successful builds
+ let allArtifacts = [];
+ let buildStatuses = {};
+
+ for (const run of buildRuns) {
+ buildStatuses[run.name] = {
+ status: run.status,
+ conclusion: run.conclusion,
+ url: run.html_url,
+ runId: run.id
+ };
+
+ if (run.conclusion === 'success') {
+ try {
+ const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: run.id
+ });
+ allArtifacts.push(...artifacts.artifacts);
+ } catch (error) {
+ console.log(`Failed to get artifacts for run ${run.id}:`, error.message);
+ }
+ }
+ }
+
+ console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
+
+ // Get artifacts from current run if needed
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId
});
- if (!artifacts || artifacts.artifacts.length === 0) {
- console.log('No artifacts found for this run');
- return;
- }
-
- // Sort and categorize artifacts
- const androidArtifacts = artifacts.artifacts
+ // Sort and categorize all collected artifacts
+ const androidArtifacts = allArtifacts
.filter(a => a.name.includes('android'))
.sort((a, b) => a.name.localeCompare(b.name));
- const iosArtifacts = artifacts.artifacts
+ const iosArtifacts = allArtifacts
.filter(a => a.name.includes('ios'))
.sort((a, b) => a.name.localeCompare(b.name));
- // Build comment body with table format
- let commentBody = `## ๐ฑ Build Artifacts Ready!\n\n`;
- commentBody += `โ
**Workflow completed successfully** for PR #${pr.number}\n`;
- commentBody += `๐ฆ **${artifacts.artifacts.length} artifacts** generated from commit [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`;
+ // Build comment body with progressive status
+ let commentBody = `## ๐ฑ Build Status for PR #${pr.number}\n\n`;
+ commentBody += `๐ **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`;
+
+ // Add build status table
+ commentBody += `### ๐ง Build Status\n\n`;
+ commentBody += `| Workflow | Status | Artifacts |\n`;
+ commentBody += `|----------|--------|-----------|\n`;
+
+ for (const [name, status] of Object.entries(buildStatuses)) {
+ const emoji = status.conclusion === 'success' ? 'โ
' :
+ status.conclusion === 'failure' ? 'โ' :
+ status.status === 'in_progress' ? '๐' : 'โณ';
+ const statusText = status.conclusion || status.status || 'pending';
+ const artifactCount = allArtifacts.filter(a => {
+ // Match artifacts to workflows based on naming patterns
+ if (name.includes('Android')) return a.name.includes('android');
+ if (name.includes('iOS')) return a.name.includes('ios');
+ return false;
+ }).length;
+
+ commentBody += `| [${name}](${status.url}) | ${emoji} ${statusText} | ${artifactCount} |\n`;
+ }
+
+ commentBody += `\n`;
+
+ // Only show download table if there are artifacts
+ if (allArtifacts.length > 0) {
+ commentBody += `### ๐ฆ Available Downloads (${allArtifacts.length} artifacts)\n\n`;
- if (androidArtifacts.length === 0 && iosArtifacts.length === 0) {
- commentBody += `โ ๏ธ No mobile app artifacts found in this build.\n\n`;
- } else {
// Create table for better organization
commentBody += `| Platform | Device Type | Download Link |\n`;
commentBody += `|----------|-------------|---------------|\n`;
@@ -115,7 +159,7 @@ jobs:
androidArtifacts.forEach(artifact => {
const isTV = artifact.name.includes('tv');
const deviceType = isTV ? '๐บ Android TV' : '๐ฑ Android Phone';
- const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
+ const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`;
commentBody += `| ๐ค Android | ${deviceType} | [๐ฅ Download APK](${nightlyLink}) |\n`;
});
@@ -123,7 +167,7 @@ jobs:
iosArtifacts.forEach(artifact => {
const isTV = artifact.name.includes('tv');
const deviceType = isTV ? '๐บ Apple TV' : '๐ฑ iPhone/iPad';
- const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`;
+ const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`;
commentBody += `| ๐ iOS | ${deviceType} | [๐ฅ Download IPA](${nightlyLink}) |\n`;
});
@@ -132,6 +176,8 @@ jobs:
commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`;
commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`;
commentBody += `> โ ๏ธ **Note**: Artifacts expire in 7 days from build date\n\n`;
+ } else {
+ commentBody += `โณ **No artifacts available yet** - builds are still in progress or haven't completed successfully.\n\n`;
}
commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})*`;
From 0d1aeaf8aaeea68f5dbb2f8a4a32eebb374db7be Mon Sep 17 00:00:00 2001
From: Uruk
Date: Mon, 29 Sep 2025 23:55:36 +0200
Subject: [PATCH 16/27] fix(ci): improve workflow run condition and artifact
collection
Fixes workflow trigger condition by explicitly checking for workflow_run event type to prevent unintended executions.
Improves artifact collection reliability by switching to the correct API method and increasing page size to capture more artifacts from multiple builds.
Removes redundant artifact fetching logic that was duplicating collection efforts.
---
.github/workflows/artifact-comment.yml | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 60244409..f7d5ff04 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -16,7 +16,7 @@ on:
jobs:
comment-artifacts:
- if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.event == 'pull_request'
+ if: github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
runs-on: ubuntu-latest
permissions:
contents: read
@@ -63,11 +63,11 @@ jobs:
console.log(`Processing PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`);
// Get all recent workflow runs for this PR to collect artifacts from multiple builds
- const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({
+ const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: pr.head.sha,
- per_page: 10
+ per_page: 20
});
// Filter for build workflows only
@@ -106,13 +106,6 @@ jobs:
console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
- // Get artifacts from current run if needed
- const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: runId
- });
-
// Sort and categorize all collected artifacts
const androidArtifacts = allArtifacts
.filter(a => a.name.includes('android'))
From 4f6863f31736f3bc5b0378d12538c101a6908963 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 00:01:15 +0200
Subject: [PATCH 17/27] feat: add direct pull request trigger to artifact
comment workflow
Enables the workflow to run directly on pull request events (opened, synchronize, reopened) in addition to the existing workflow_run and manual dispatch triggers.
Provides immediate status updates in PR checks and improves user experience by showing workflow progress directly in the pull request interface rather than only after completion of upstream workflows.
---
.github/workflows/artifact-comment.yml | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index f7d5ff04..4d7df677 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -6,6 +6,8 @@ concurrency:
on:
workflow_dispatch: # Allow manual testing
+ pull_request: # Show in PR checks and provide status updates
+ types: [opened, synchronize, reopened]
workflow_run:
workflows:
- "๐ค Android APK Build (Phone + TV)"
@@ -16,7 +18,7 @@ on:
jobs:
comment-artifacts:
- if: github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
+ if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
runs-on: ubuntu-latest
permissions:
contents: read
@@ -28,7 +30,7 @@ jobs:
uses: actions/github-script@v8
with:
script: |
- // Handle both workflow_run and manual dispatch events
+ // Handle workflow_run, pull_request, and manual dispatch events
let pr;
if (context.eventName === 'workflow_run') {
@@ -45,6 +47,10 @@ jobs:
}
pr = pullRequests[0];
+ } else if (context.eventName === 'pull_request') {
+ // Direct PR event
+ pr = context.payload.pull_request;
+
} else if (context.eventName === 'workflow_dispatch') {
// Get current PR for manual testing
const prNumber = context.payload.pull_request?.number || 1101;
From 5b0d2f3f7bf158c019106edcb78be97ce6b89e02 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 00:03:40 +0200
Subject: [PATCH 18/27] fix(ci): correct GitHub Actions run ID reference
Fixes incorrect variable reference in artifact comment generation that was using undefined `runId` instead of the proper `context.runId` from the GitHub Actions context.
Also adds descriptive job name for better workflow visibility.
---
.github/workflows/artifact-comment.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 4d7df677..1e63db57 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -19,6 +19,7 @@ on:
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
permissions:
contents: read
@@ -179,7 +180,7 @@ jobs:
commentBody += `โณ **No artifacts available yet** - builds are still in progress or haven't completed successfully.\n\n`;
}
- commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})*`;
+ commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`;
commentBody += `\n`;
// Find existing bot comment to update
From e985adf06226e06fb58a2cca6b246ed6145a63cf Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 00:22:51 +0200
Subject: [PATCH 19/27] feat: improve build status UI with progressive updates
Restructures the artifact comment workflow to display build progress in real-time with individual platform/device status tracking.
Changes the status table from workflow-based to target-based (Android Phone/TV, iOS Phone/TV) with dedicated status indicators and download links that update as builds complete.
Improves user experience by showing pending builds with appropriate messaging instead of waiting for all builds to finish before displaying any information.
---
.github/workflows/artifact-comment.yml | 111 +++++++++++++------------
1 file changed, 59 insertions(+), 52 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 1e63db57..6a36d0f5 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -85,10 +85,18 @@ jobs:
console.log(`Found ${buildRuns.length} build workflow runs for this commit`);
- // Collect artifacts from all completed successful builds
+ // Collect artifacts and statuses from all builds (completed and in-progress)
let allArtifacts = [];
let buildStatuses = {};
+ // Define all expected build targets
+ const expectedBuilds = {
+ 'Android Phone': { platform: 'Android', device: 'Phone', emoji: '๐ฑ', pattern: 'android.*phone' },
+ 'Android TV': { platform: 'Android', device: 'TV', emoji: '๐บ', pattern: 'android.*tv' },
+ 'iOS Phone': { platform: 'iOS', device: 'Phone', emoji: '๐ฑ', pattern: 'ios.*phone' },
+ 'iOS TV': { platform: 'iOS', device: 'TV', emoji: '๐บ', pattern: 'ios.*tv' }
+ };
+
for (const run of buildRuns) {
buildStatuses[run.name] = {
status: run.status,
@@ -97,6 +105,7 @@ jobs:
runId: run.id
};
+ // Collect artifacts from any completed successful builds
if (run.conclusion === 'success') {
try {
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
@@ -113,71 +122,69 @@ jobs:
console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
- // Sort and categorize all collected artifacts
- const androidArtifacts = allArtifacts
- .filter(a => a.name.includes('android'))
- .sort((a, b) => a.name.localeCompare(b.name));
- const iosArtifacts = allArtifacts
- .filter(a => a.name.includes('ios'))
- .sort((a, b) => a.name.localeCompare(b.name));
-
- // Build comment body with progressive status
+ // Build comment body with progressive status for individual builds
let commentBody = `## ๐ฑ Build Status for PR #${pr.number}\n\n`;
commentBody += `๐ **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`;
- // Add build status table
- commentBody += `### ๐ง Build Status\n\n`;
- commentBody += `| Workflow | Status | Artifacts |\n`;
- commentBody += `|----------|--------|-----------|\n`;
+ // Progressive build status and downloads table
+ commentBody += `### ๏ฟฝ Build Artifacts\n\n`;
+ commentBody += `| Platform | Device | Status | Download |\n`;
+ commentBody += `|----------|--------|--------|---------|\n`;
- for (const [name, status] of Object.entries(buildStatuses)) {
- const emoji = status.conclusion === 'success' ? 'โ
' :
- status.conclusion === 'failure' ? 'โ' :
- status.status === 'in_progress' ? '๐' : 'โณ';
- const statusText = status.conclusion || status.status || 'pending';
- const artifactCount = allArtifacts.filter(a => {
- // Match artifacts to workflows based on naming patterns
- if (name.includes('Android')) return a.name.includes('android');
- if (name.includes('iOS')) return a.name.includes('ios');
- return false;
- }).length;
-
- commentBody += `| [${name}](${status.url}) | ${emoji} ${statusText} | ${artifactCount} |\n`;
+ // Process each expected build target individually
+ const buildTargets = [
+ { name: 'Android Phone', platform: '๐ค', device: '๐ฑ', pattern: /android.*phone/i },
+ { name: 'Android TV', platform: '๐ค', device: '๐บ', pattern: /android.*tv/i },
+ { name: 'iOS Phone', platform: '๐', device: '๏ฟฝ', pattern: /ios.*phone/i },
+ { name: 'iOS TV', platform: '๐', device: '๐บ', pattern: /ios.*tv/i }
+ ];
+
+ for (const target of buildTargets) {
+ // Find matching workflow run
+ const matchingRun = buildRuns.find(run => {
+ return (run.name.includes('Android') && target.name.includes('Android')) ||
+ (run.name.includes('iOS') && target.name.includes('iOS'));
+ });
+
+ // Find matching artifact
+ const matchingArtifact = allArtifacts.find(artifact =>
+ target.pattern.test(artifact.name)
+ );
+
+ let status = 'โณ Pending';
+ let downloadLink = '*Waiting for build...*';
+
+ if (matchingRun) {
+ if (matchingRun.conclusion === 'success' && matchingArtifact) {
+ status = 'โ
Complete';
+ const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`;
+ const fileType = target.name.includes('Android') ? 'APK' : 'IPA';
+ downloadLink = `[๐ฅ Download ${fileType}](${nightlyLink})`;
+ } else if (matchingRun.conclusion === 'failure') {
+ status = `โ [Failed](${matchingRun.html_url})`;
+ downloadLink = '*Build failed*';
+ } else if (matchingRun.status === 'in_progress') {
+ status = `๐ [Building...](${matchingRun.html_url})`;
+ downloadLink = '*Build in progress...*';
+ } else if (matchingRun.status === 'queued') {
+ status = `โณ [Queued](${matchingRun.html_url})`;
+ downloadLink = '*Waiting to start...*';
+ }
+ }
+
+ commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`;
}
commentBody += `\n`;
- // Only show download table if there are artifacts
+ // Show installation instructions if we have any artifacts
if (allArtifacts.length > 0) {
- commentBody += `### ๐ฆ Available Downloads (${allArtifacts.length} artifacts)\n\n`;
-
- // Create table for better organization
- commentBody += `| Platform | Device Type | Download Link |\n`;
- commentBody += `|----------|-------------|---------------|\n`;
-
- // Add Android artifacts
- androidArtifacts.forEach(artifact => {
- const isTV = artifact.name.includes('tv');
- const deviceType = isTV ? '๐บ Android TV' : '๐ฑ Android Phone';
- const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`;
- commentBody += `| ๐ค Android | ${deviceType} | [๐ฅ Download APK](${nightlyLink}) |\n`;
- });
-
- // Add iOS artifacts
- iosArtifacts.forEach(artifact => {
- const isTV = artifact.name.includes('tv');
- const deviceType = isTV ? '๐บ Apple TV' : '๐ฑ iPhone/iPad';
- const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`;
- commentBody += `| ๐ iOS | ${deviceType} | [๐ฅ Download IPA](${nightlyLink}) |\n`;
- });
-
- commentBody += `\n`;
commentBody += `### ๐ง Installation Instructions\n\n`;
commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`;
commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`;
commentBody += `> โ ๏ธ **Note**: Artifacts expire in 7 days from build date\n\n`;
} else {
- commentBody += `โณ **No artifacts available yet** - builds are still in progress or haven't completed successfully.\n\n`;
+ commentBody += `โณ **Builds are starting up...** This comment will update automatically as each build completes.\n\n`;
}
commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`;
From 44e489f40c66deac09ae9e93d5640301aff57496 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 00:29:13 +0200
Subject: [PATCH 20/27] refactor: improve workflow run tracking and status
display
Enhances the artifact comment workflow by switching from tracking all build runs to focusing on the most recent run per workflow type (Android/iOS).
Changes include:
- Increases pagination limit to capture more workflow runs
- Sorts runs by creation time to identify latest builds
- Simplifies status tracking by workflow platform rather than individual runs
- Adds detailed logging for debugging build statuses
- Improves error handling for artifact collection
- Fixes emoji rendering issue in status display
Reduces complexity while ensuring accurate status reporting for the latest builds.
---
.github/workflows/artifact-comment.yml | 112 ++++++++++++++++---------
1 file changed, 72 insertions(+), 40 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 6a36d0f5..1aa981f1 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -74,48 +74,79 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: pr.head.sha,
- per_page: 20
+ per_page: 30
});
- // Filter for build workflows only
- const buildRuns = workflowRuns.workflow_runs.filter(run =>
- run.name.includes('Android APK Build') ||
- run.name.includes('iOS IPA Build')
- );
+ // Filter for build workflows only and sort by creation time (most recent first)
+ const buildRuns = workflowRuns.workflow_runs
+ .filter(run =>
+ run.name.includes('Android APK Build') ||
+ run.name.includes('iOS IPA Build')
+ )
+ .sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
console.log(`Found ${buildRuns.length} build workflow runs for this commit`);
- // Collect artifacts and statuses from all builds (completed and in-progress)
+ // Log current status of each build for debugging
+ buildRuns.forEach(run => {
+ console.log(`- ${run.name}: ${run.status} (${run.conclusion || 'no conclusion yet'}) - Created: ${run.created_at}`);
+ });
+
+ // Collect artifacts and statuses from builds - get most recent run for each workflow type
let allArtifacts = [];
let buildStatuses = {};
- // Define all expected build targets
- const expectedBuilds = {
- 'Android Phone': { platform: 'Android', device: 'Phone', emoji: '๐ฑ', pattern: 'android.*phone' },
- 'Android TV': { platform: 'Android', device: 'TV', emoji: '๐บ', pattern: 'android.*tv' },
- 'iOS Phone': { platform: 'iOS', device: 'Phone', emoji: '๐ฑ', pattern: 'ios.*phone' },
- 'iOS TV': { platform: 'iOS', device: 'TV', emoji: '๐บ', pattern: 'ios.*tv' }
- };
+ // Get the most recent run for each workflow type
+ const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build'));
+ const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build'));
- for (const run of buildRuns) {
- buildStatuses[run.name] = {
- status: run.status,
- conclusion: run.conclusion,
- url: run.html_url,
- runId: run.id
+ // Store status for each workflow type
+ if (latestAndroidRun) {
+ buildStatuses['Android'] = {
+ name: latestAndroidRun.name,
+ status: latestAndroidRun.status,
+ conclusion: latestAndroidRun.conclusion,
+ url: latestAndroidRun.html_url,
+ runId: latestAndroidRun.id,
+ created_at: latestAndroidRun.created_at
};
- // Collect artifacts from any completed successful builds
- if (run.conclusion === 'success') {
+ // Collect artifacts if completed successfully
+ if (latestAndroidRun.conclusion === 'success') {
try {
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
- run_id: run.id
+ run_id: latestAndroidRun.id
});
allArtifacts.push(...artifacts.artifacts);
} catch (error) {
- console.log(`Failed to get artifacts for run ${run.id}:`, error.message);
+ console.log(`Failed to get Android artifacts for run ${latestAndroidRun.id}:`, error.message);
+ }
+ }
+ }
+
+ if (latestIOSRun) {
+ buildStatuses['iOS'] = {
+ name: latestIOSRun.name,
+ status: latestIOSRun.status,
+ conclusion: latestIOSRun.conclusion,
+ url: latestIOSRun.html_url,
+ runId: latestIOSRun.id,
+ created_at: latestIOSRun.created_at
+ };
+
+ // Collect artifacts if completed successfully
+ if (latestIOSRun.conclusion === 'success') {
+ try {
+ const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: latestIOSRun.id
+ });
+ allArtifacts.push(...artifacts.artifacts);
+ } catch (error) {
+ console.log(`Failed to get iOS artifacts for run ${latestIOSRun.id}:`, error.message);
}
}
}
@@ -123,11 +154,11 @@ jobs:
console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
// Build comment body with progressive status for individual builds
- let commentBody = `## ๐ฑ Build Status for PR #${pr.number}\n\n`;
+ let commentBody = `## ๐ง Build Status for PR #${pr.number}\n\n`;
commentBody += `๐ **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`;
// Progressive build status and downloads table
- commentBody += `### ๏ฟฝ Build Artifacts\n\n`;
+ commentBody += `### ๐ฆ Build Artifacts\n\n`;
commentBody += `| Platform | Device | Status | Download |\n`;
commentBody += `|----------|--------|--------|---------|\n`;
@@ -135,16 +166,13 @@ jobs:
const buildTargets = [
{ name: 'Android Phone', platform: '๐ค', device: '๐ฑ', pattern: /android.*phone/i },
{ name: 'Android TV', platform: '๐ค', device: '๐บ', pattern: /android.*tv/i },
- { name: 'iOS Phone', platform: '๐', device: '๏ฟฝ', pattern: /ios.*phone/i },
+ { name: 'iOS Phone', platform: '๐', device: '๐ฑ', pattern: /ios.*phone/i },
{ name: 'iOS TV', platform: '๐', device: '๐บ', pattern: /ios.*tv/i }
];
for (const target of buildTargets) {
- // Find matching workflow run
- const matchingRun = buildRuns.find(run => {
- return (run.name.includes('Android') && target.name.includes('Android')) ||
- (run.name.includes('iOS') && target.name.includes('iOS'));
- });
+ // Find matching workflow status (using our simplified structure)
+ const matchingStatus = target.name.includes('Android') ? buildStatuses['Android'] : buildStatuses['iOS'];
// Find matching artifact
const matchingArtifact = allArtifacts.find(artifact =>
@@ -154,21 +182,25 @@ jobs:
let status = 'โณ Pending';
let downloadLink = '*Waiting for build...*';
- if (matchingRun) {
- if (matchingRun.conclusion === 'success' && matchingArtifact) {
+ if (matchingStatus) {
+ if (matchingStatus.conclusion === 'success' && matchingArtifact) {
status = 'โ
Complete';
const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`;
const fileType = target.name.includes('Android') ? 'APK' : 'IPA';
downloadLink = `[๐ฅ Download ${fileType}](${nightlyLink})`;
- } else if (matchingRun.conclusion === 'failure') {
- status = `โ [Failed](${matchingRun.html_url})`;
+ } else if (matchingStatus.conclusion === 'failure') {
+ status = `โ [Failed](${matchingStatus.url})`;
downloadLink = '*Build failed*';
- } else if (matchingRun.status === 'in_progress') {
- status = `๐ [Building...](${matchingRun.html_url})`;
+ } else if (matchingStatus.status === 'in_progress') {
+ status = `๐ [Building...](${matchingStatus.url})`;
downloadLink = '*Build in progress...*';
- } else if (matchingRun.status === 'queued') {
- status = `โณ [Queued](${matchingRun.html_url})`;
+ } else if (matchingStatus.status === 'queued') {
+ status = `โณ [Queued](${matchingStatus.url})`;
downloadLink = '*Waiting to start...*';
+ } else {
+ // Show any other status with timestamp for debugging
+ status = `๐ [${matchingStatus.status}](${matchingStatus.url})`;
+ downloadLink = `*Status: ${matchingStatus.status}*`;
}
}
From 2b761f15c82a0ae522bb3e3944f896aef4bf3846 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 00:51:02 +0200
Subject: [PATCH 21/27] feat!: replace workflow_run with repository_dispatch
for real-time build status updates
Replaces the workflow_run trigger mechanism with repository_dispatch events to enable real-time build status communication between build workflows and the artifact comment system.
Build workflows now actively notify the comment workflow when builds start, complete, or fail, providing immediate status updates rather than polling for completed workflows.
Adds real-time payload processing to display current build status and target information in PR comments, improving visibility into ongoing build processes.
BREAKING CHANGE: Changes the trigger mechanism from workflow_run to repository_dispatch, requiring build workflows to explicitly send status notifications.
---
.github/workflows/artifact-comment.yml | 110 ++++++++++++++++++-------
.github/workflows/build-android.yml | 61 ++++++++++++++
.github/workflows/build-ios.yml | 67 ++++++++++++++-
3 files changed, 207 insertions(+), 31 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 1aa981f1..0bfb99af 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -1,56 +1,61 @@
name: ๐ Artifact Comment on PR
concurrency:
- group: artifact-comment-${{ github.event.workflow_run.head_sha || github.sha }}
+ group: artifact-comment-${{ github.sha }}
cancel-in-progress: true
on:
workflow_dispatch: # Allow manual testing
pull_request: # Show in PR checks and provide status updates
types: [opened, synchronize, reopened]
- workflow_run:
- workflows:
- - "๐ค Android APK Build (Phone + TV)"
- - "๐ค iOS IPA Build (Phone + TV)"
+ repository_dispatch: # Triggered by build workflows when they start/complete
types:
- - completed
- - requested # Trigger when build starts
+ - build-started
+ - build-completed
+ - build-failed
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')
+ if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'repository_dispatch'
name: ๐ฆ Post Build Artifacts
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
actions: read
+ repository-projects: read
steps:
- name: ๐ Get PR and Artifacts
uses: actions/github-script@v8
with:
script: |
- // Handle workflow_run, pull_request, and manual dispatch events
+ // Handle repository_dispatch, pull_request, and manual dispatch events
let pr;
+ let targetCommitSha;
- if (context.eventName === 'workflow_run') {
- // Find PR associated with this commit
- const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
- owner: context.repo.owner,
- repo: context.repo.repo,
- commit_sha: github.event.workflow_run.head_sha
- });
+ if (context.eventName === 'repository_dispatch') {
+ // Triggered by build workflows - get PR info from payload
+ const payload = context.payload.client_payload;
+ console.log('Repository dispatch payload:', JSON.stringify(payload, null, 2));
- if (pullRequests.length === 0) {
- console.log('No pull request found for commit:', github.event.workflow_run.head_sha);
+ if (!payload || !payload.pr_number) {
+ console.log('No PR information in repository_dispatch payload');
return;
}
- pr = pullRequests[0];
+
+ const { data: prData } = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: payload.pr_number
+ });
+ pr = prData;
+ targetCommitSha = payload.commit_sha || pr.head.sha;
} else if (context.eventName === 'pull_request') {
// Direct PR event
pr = context.payload.pull_request;
+ targetCommitSha = pr.head.sha;
} else if (context.eventName === 'workflow_dispatch') {
// Get current PR for manual testing
@@ -61,19 +66,20 @@ jobs:
pull_number: prNumber
});
pr = prData;
+ targetCommitSha = pr.head.sha;
} else {
console.log('Unsupported event type:', context.eventName);
return;
}
- console.log(`Processing PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`);
+ console.log(`Processing PR #${pr.number} for commit ${targetCommitSha.substring(0, 7)}`);
// Get all recent workflow runs for this PR to collect artifacts from multiple builds
const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
- head_sha: pr.head.sha,
+ head_sha: targetCommitSha,
per_page: 30
});
@@ -151,11 +157,50 @@ jobs:
}
}
+ // Override with real-time data from repository_dispatch if available
+ if (context.eventName === 'repository_dispatch') {
+ const payload = context.payload.client_payload;
+ const workflowType = payload.workflow_name.includes('Android') ? 'Android' : 'iOS';
+
+ if (buildStatuses[workflowType]) {
+ // Update the existing status with real-time data
+ buildStatuses[workflowType].status = payload.status === 'in_progress' ? 'in_progress' :
+ payload.status === 'success' ? 'completed' :
+ payload.status === 'failure' ? 'completed' :
+ buildStatuses[workflowType].status;
+ buildStatuses[workflowType].conclusion = payload.status === 'success' ? 'success' :
+ payload.status === 'failure' ? 'failure' :
+ buildStatuses[workflowType].conclusion;
+ buildStatuses[workflowType].url = payload.run_url;
+ buildStatuses[workflowType].target = payload.target;
+ } else {
+ // Create new status entry for real-time updates
+ buildStatuses[workflowType] = {
+ name: payload.workflow_name,
+ status: payload.status === 'in_progress' ? 'in_progress' :
+ payload.status === 'success' ? 'completed' :
+ payload.status === 'failure' ? 'completed' : 'queued',
+ conclusion: payload.status === 'success' ? 'success' :
+ payload.status === 'failure' ? 'failure' : null,
+ url: payload.run_url,
+ runId: payload.run_id,
+ target: payload.target,
+ created_at: new Date().toISOString()
+ };
+ }
+ }
+
console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
// Build comment body with progressive status for individual builds
let commentBody = `## ๐ง Build Status for PR #${pr.number}\n\n`;
- commentBody += `๐ **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`;
+ commentBody += `๐ **Commit**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\n\n`;
+
+ // Show event context for debugging (only for repository_dispatch)
+ if (context.eventName === 'repository_dispatch') {
+ const payload = context.payload.client_payload;
+ commentBody += `๐ **Real-time Update**: ${payload.workflow_name} (${payload.target}) - ${payload.status}\n\n`;
+ }
// Progressive build status and downloads table
commentBody += `### ๐ฆ Build Artifacts\n\n`;
@@ -164,15 +209,24 @@ jobs:
// Process each expected build target individually
const buildTargets = [
- { name: 'Android Phone', platform: '๐ค', device: '๐ฑ', pattern: /android.*phone/i },
- { name: 'Android TV', platform: '๐ค', device: '๐บ', pattern: /android.*tv/i },
- { name: 'iOS Phone', platform: '๐', device: '๐ฑ', pattern: /ios.*phone/i },
- { name: 'iOS TV', platform: '๐', device: '๐บ', pattern: /ios.*tv/i }
+ { name: 'Android Phone', platform: '๐ค', device: '๐ฑ', workflowType: 'Android', target: 'phone' },
+ { name: 'Android TV', platform: '๐ค', device: '๐บ', workflowType: 'Android', target: 'tv' },
+ { name: 'iOS Phone', platform: '๐', device: '๐ฑ', workflowType: 'iOS', target: 'phone' },
+ { name: 'iOS TV', platform: '๐', device: '๐บ', workflowType: 'iOS', target: 'tv' }
];
for (const target of buildTargets) {
- // Find matching workflow status (using our simplified structure)
- const matchingStatus = target.name.includes('Android') ? buildStatuses['Android'] : buildStatuses['iOS'];
+ // Find matching workflow status
+ let matchingStatus = buildStatuses[target.workflowType];
+
+ // For repository_dispatch events, check if this specific target matches
+ if (context.eventName === 'repository_dispatch' && matchingStatus) {
+ const payload = context.payload.client_payload;
+ if (payload.target !== target.target) {
+ // This update is for a different target, show default status
+ matchingStatus = null;
+ }
+ }
// Find matching artifact
const matchingArtifact = allArtifacts.find(artifact =>
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
index bc9b9ea1..d273f616 100644
--- a/.github/workflows/build-android.yml
+++ b/.github/workflows/build-android.yml
@@ -18,6 +18,7 @@ jobs:
name: ๐๏ธ Build Android APK
permissions:
contents: read
+ repository-projects: write
strategy:
fail-fast: false
@@ -25,6 +26,26 @@ jobs:
target: [phone, tv]
steps:
+ - name: ๐ข Notify artifact comment workflow (started)
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ await github.rest.repos.createDispatchEvent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ event_type: 'build-started',
+ client_payload: {
+ workflow_name: 'Android APK Build',
+ target: '${{ matrix.target }}',
+ status: 'in_progress',
+ pr_number: ${{ github.event.pull_request.number }},
+ commit_sha: '${{ github.event.pull_request.head.sha }}',
+ run_id: ${{ github.run_id }},
+ run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+ }
+ });
+
- name: ๐ฅ Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -91,3 +112,43 @@ jobs:
path: |
android/app/build/outputs/apk/release/*.apk
retention-days: 7
+
+ - name: ๐ Notify artifact comment workflow (success)
+ if: success() && github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ await github.rest.repos.createDispatchEvent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ event_type: 'build-completed',
+ client_payload: {
+ workflow_name: 'Android APK Build',
+ target: '${{ matrix.target }}',
+ status: 'success',
+ pr_number: ${{ github.event.pull_request.number }},
+ commit_sha: '${{ github.event.pull_request.head.sha }}',
+ run_id: ${{ github.run_id }},
+ run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+ }
+ });
+
+ - name: ๐ Notify artifact comment workflow (failure)
+ if: failure() && github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ await github.rest.repos.createDispatchEvent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ event_type: 'build-failed',
+ client_payload: {
+ workflow_name: 'Android APK Build',
+ target: '${{ matrix.target }}',
+ status: 'failure',
+ pr_number: ${{ github.event.pull_request.number }},
+ commit_sha: '${{ github.event.pull_request.head.sha }}',
+ run_id: ${{ github.run_id }},
+ run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+ }
+ });
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
index 98b587b3..27c5be6c 100644
--- a/.github/workflows/build-ios.yml
+++ b/.github/workflows/build-ios.yml
@@ -9,11 +9,11 @@ on:
pull_request:
branches: [develop, master]
paths-ignore:
- - '*.md'
+ - "*.md"
push:
branches: [develop, master]
paths-ignore:
- - '*.md'
+ - "*.md"
jobs:
build-ios:
@@ -22,14 +22,35 @@ jobs:
name: ๐๏ธ Build iOS IPA
permissions:
contents: read
+ repository-projects: write
strategy:
fail-fast: false
matrix:
target: [phone]
-# target: [phone, tv]
+ # target: [phone, tv]
steps:
+ - name: ๐ข Notify artifact comment workflow (started)
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ await github.rest.repos.createDispatchEvent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ event_type: 'build-started',
+ client_payload: {
+ workflow_name: 'iOS IPA Build',
+ target: '${{ matrix.target }}',
+ status: 'in_progress',
+ pr_number: ${{ github.event.pull_request.number }},
+ commit_sha: '${{ github.event.pull_request.head.sha }}',
+ run_id: ${{ github.run_id }},
+ run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+ }
+ });
+
- name: ๐ฅ Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -93,3 +114,43 @@ jobs:
name: streamyfin-ios-${{ matrix.target }}-ipa-${{ env.DATE_TAG }}
path: build-*.ipa
retention-days: 7
+
+ - name: ๐ Notify artifact comment workflow (success)
+ if: success() && github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ await github.rest.repos.createDispatchEvent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ event_type: 'build-completed',
+ client_payload: {
+ workflow_name: 'iOS IPA Build',
+ target: '${{ matrix.target }}',
+ status: 'success',
+ pr_number: ${{ github.event.pull_request.number }},
+ commit_sha: '${{ github.event.pull_request.head.sha }}',
+ run_id: ${{ github.run_id }},
+ run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+ }
+ });
+
+ - name: ๐ Notify artifact comment workflow (failure)
+ if: failure() && github.event_name == 'pull_request'
+ uses: actions/github-script@v8
+ with:
+ script: |
+ await github.rest.repos.createDispatchEvent({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ event_type: 'build-failed',
+ client_payload: {
+ workflow_name: 'iOS IPA Build',
+ target: '${{ matrix.target }}',
+ status: 'failure',
+ pr_number: ${{ github.event.pull_request.number }},
+ commit_sha: '${{ github.event.pull_request.head.sha }}',
+ run_id: ${{ github.run_id }},
+ run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
+ }
+ });
From 788f420ce58997567cb54422f78a094937f25ec6 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 00:58:25 +0200
Subject: [PATCH 22/27] refactor: migrate artifact comment system from
repository_dispatch to workflow_run
Replaces the repository_dispatch event system with workflow_run triggers to improve reliability and reduce complexity. The new approach automatically detects PR associations through commit SHA lookups rather than requiring manual payload construction.
Removes redundant notification steps from build workflows and simplifies the concurrency group logic. Enhances manual testing support with improved PR discovery fallbacks.
---
.github/workflows/artifact-comment.yml | 142 +++++++++++--------------
.github/workflows/build-android.yml | 61 -----------
.github/workflows/build-ios.yml | 61 -----------
3 files changed, 61 insertions(+), 203 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 0bfb99af..c4773b62 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -1,29 +1,29 @@
name: ๐ Artifact Comment on PR
concurrency:
- group: artifact-comment-${{ github.sha }}
+ group: artifact-comment-${{ github.event.workflow_run.head_sha || github.sha }}
cancel-in-progress: true
on:
workflow_dispatch: # Allow manual testing
pull_request: # Show in PR checks and provide status updates
types: [opened, synchronize, reopened]
- repository_dispatch: # Triggered by build workflows when they start/complete
+ workflow_run: # Triggered when build workflows complete
+ workflows:
+ - "๐ค Android APK Build (Phone + TV)"
+ - "๐ค iOS IPA Build (Phone + TV)"
types:
- - build-started
- - build-completed
- - build-failed
+ - completed
jobs:
comment-artifacts:
- if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'repository_dispatch'
+ 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
permissions:
contents: read
pull-requests: write
actions: read
- repository-projects: read
steps:
- name: ๐ Get PR and Artifacts
@@ -34,23 +34,23 @@ jobs:
let pr;
let targetCommitSha;
- if (context.eventName === 'repository_dispatch') {
- // Triggered by build workflows - get PR info from payload
- const payload = context.payload.client_payload;
- console.log('Repository dispatch payload:', JSON.stringify(payload, null, 2));
+ if (context.eventName === 'workflow_run') {
+ // Find PR associated with this workflow run commit
+ console.log('Workflow run event:', context.payload.workflow_run.name);
- if (!payload || !payload.pr_number) {
- console.log('No PR information in repository_dispatch payload');
+ const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ commit_sha: context.payload.workflow_run.head_sha
+ });
+
+ if (pullRequests.length === 0) {
+ console.log('No pull request found for commit:', context.payload.workflow_run.head_sha);
return;
}
- const { data: prData } = await github.rest.pulls.get({
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: payload.pr_number
- });
- pr = prData;
- targetCommitSha = payload.commit_sha || pr.head.sha;
+ pr = pullRequests[0];
+ targetCommitSha = context.payload.workflow_run.head_sha;
} else if (context.eventName === 'pull_request') {
// Direct PR event
@@ -58,15 +58,45 @@ jobs:
targetCommitSha = pr.head.sha;
} else if (context.eventName === 'workflow_dispatch') {
- // Get current PR for manual testing
- const prNumber = context.payload.pull_request?.number || 1101;
- const { data: prData } = await github.rest.pulls.get({
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: prNumber
- });
- pr = prData;
- targetCommitSha = pr.head.sha;
+ // For manual testing, try to find PR for current branch/commit
+ console.log('Manual workflow dispatch triggered');
+
+ // First, try to find PRs associated with current commit
+ try {
+ const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ commit_sha: context.sha
+ });
+
+ if (pullRequests.length > 0) {
+ pr = pullRequests[0];
+ targetCommitSha = pr.head.sha;
+ console.log(`Found PR #${pr.number} for commit ${context.sha.substring(0, 7)}`);
+ } else {
+ // Fallback: get latest open PR
+ const { data: openPRs } = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ sort: 'updated',
+ direction: 'desc',
+ per_page: 1
+ });
+
+ if (openPRs.length > 0) {
+ pr = openPRs[0];
+ targetCommitSha = pr.head.sha;
+ console.log(`Using latest open PR #${pr.number} for manual testing`);
+ } else {
+ console.log('No open PRs found for manual testing');
+ return;
+ }
+ }
+ } catch (error) {
+ console.log('Error finding PR for manual testing:', error.message);
+ return;
+ }
} else {
console.log('Unsupported event type:', context.eventName);
@@ -157,52 +187,11 @@ jobs:
}
}
- // Override with real-time data from repository_dispatch if available
- if (context.eventName === 'repository_dispatch') {
- const payload = context.payload.client_payload;
- const workflowType = payload.workflow_name.includes('Android') ? 'Android' : 'iOS';
-
- if (buildStatuses[workflowType]) {
- // Update the existing status with real-time data
- buildStatuses[workflowType].status = payload.status === 'in_progress' ? 'in_progress' :
- payload.status === 'success' ? 'completed' :
- payload.status === 'failure' ? 'completed' :
- buildStatuses[workflowType].status;
- buildStatuses[workflowType].conclusion = payload.status === 'success' ? 'success' :
- payload.status === 'failure' ? 'failure' :
- buildStatuses[workflowType].conclusion;
- buildStatuses[workflowType].url = payload.run_url;
- buildStatuses[workflowType].target = payload.target;
- } else {
- // Create new status entry for real-time updates
- buildStatuses[workflowType] = {
- name: payload.workflow_name,
- status: payload.status === 'in_progress' ? 'in_progress' :
- payload.status === 'success' ? 'completed' :
- payload.status === 'failure' ? 'completed' : 'queued',
- conclusion: payload.status === 'success' ? 'success' :
- payload.status === 'failure' ? 'failure' : null,
- url: payload.run_url,
- runId: payload.run_id,
- target: payload.target,
- created_at: new Date().toISOString()
- };
- }
- }
-
console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
// 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`;
-
- // Show event context for debugging (only for repository_dispatch)
- if (context.eventName === 'repository_dispatch') {
- const payload = context.payload.client_payload;
- commentBody += `๐ **Real-time Update**: ${payload.workflow_name} (${payload.target}) - ${payload.status}\n\n`;
- }
-
- // Progressive build status and downloads table
+ 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
commentBody += `### ๐ฆ Build Artifacts\n\n`;
commentBody += `| Platform | Device | Status | Download |\n`;
commentBody += `|----------|--------|--------|---------|\n`;
@@ -217,16 +206,7 @@ jobs:
for (const target of buildTargets) {
// Find matching workflow status
- let matchingStatus = buildStatuses[target.workflowType];
-
- // For repository_dispatch events, check if this specific target matches
- if (context.eventName === 'repository_dispatch' && matchingStatus) {
- const payload = context.payload.client_payload;
- if (payload.target !== target.target) {
- // This update is for a different target, show default status
- matchingStatus = null;
- }
- }
+ const matchingStatus = buildStatuses[target.workflowType];
// Find matching artifact
const matchingArtifact = allArtifacts.find(artifact =>
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
index d273f616..bc9b9ea1 100644
--- a/.github/workflows/build-android.yml
+++ b/.github/workflows/build-android.yml
@@ -18,7 +18,6 @@ jobs:
name: ๐๏ธ Build Android APK
permissions:
contents: read
- repository-projects: write
strategy:
fail-fast: false
@@ -26,26 +25,6 @@ jobs:
target: [phone, tv]
steps:
- - name: ๐ข Notify artifact comment workflow (started)
- if: github.event_name == 'pull_request'
- uses: actions/github-script@v8
- with:
- script: |
- await github.rest.repos.createDispatchEvent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- event_type: 'build-started',
- client_payload: {
- workflow_name: 'Android APK Build',
- target: '${{ matrix.target }}',
- status: 'in_progress',
- pr_number: ${{ github.event.pull_request.number }},
- commit_sha: '${{ github.event.pull_request.head.sha }}',
- run_id: ${{ github.run_id }},
- run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
- }
- });
-
- name: ๐ฅ Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -112,43 +91,3 @@ jobs:
path: |
android/app/build/outputs/apk/release/*.apk
retention-days: 7
-
- - name: ๐ Notify artifact comment workflow (success)
- if: success() && github.event_name == 'pull_request'
- uses: actions/github-script@v8
- with:
- script: |
- await github.rest.repos.createDispatchEvent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- event_type: 'build-completed',
- client_payload: {
- workflow_name: 'Android APK Build',
- target: '${{ matrix.target }}',
- status: 'success',
- pr_number: ${{ github.event.pull_request.number }},
- commit_sha: '${{ github.event.pull_request.head.sha }}',
- run_id: ${{ github.run_id }},
- run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
- }
- });
-
- - name: ๐ Notify artifact comment workflow (failure)
- if: failure() && github.event_name == 'pull_request'
- uses: actions/github-script@v8
- with:
- script: |
- await github.rest.repos.createDispatchEvent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- event_type: 'build-failed',
- client_payload: {
- workflow_name: 'Android APK Build',
- target: '${{ matrix.target }}',
- status: 'failure',
- pr_number: ${{ github.event.pull_request.number }},
- commit_sha: '${{ github.event.pull_request.head.sha }}',
- run_id: ${{ github.run_id }},
- run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
- }
- });
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
index 27c5be6c..76e4a4d2 100644
--- a/.github/workflows/build-ios.yml
+++ b/.github/workflows/build-ios.yml
@@ -22,7 +22,6 @@ jobs:
name: ๐๏ธ Build iOS IPA
permissions:
contents: read
- repository-projects: write
strategy:
fail-fast: false
@@ -31,26 +30,6 @@ jobs:
# target: [phone, tv]
steps:
- - name: ๐ข Notify artifact comment workflow (started)
- if: github.event_name == 'pull_request'
- uses: actions/github-script@v8
- with:
- script: |
- await github.rest.repos.createDispatchEvent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- event_type: 'build-started',
- client_payload: {
- workflow_name: 'iOS IPA Build',
- target: '${{ matrix.target }}',
- status: 'in_progress',
- pr_number: ${{ github.event.pull_request.number }},
- commit_sha: '${{ github.event.pull_request.head.sha }}',
- run_id: ${{ github.run_id }},
- run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
- }
- });
-
- name: ๐ฅ Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -114,43 +93,3 @@ jobs:
name: streamyfin-ios-${{ matrix.target }}-ipa-${{ env.DATE_TAG }}
path: build-*.ipa
retention-days: 7
-
- - name: ๐ Notify artifact comment workflow (success)
- if: success() && github.event_name == 'pull_request'
- uses: actions/github-script@v8
- with:
- script: |
- await github.rest.repos.createDispatchEvent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- event_type: 'build-completed',
- client_payload: {
- workflow_name: 'iOS IPA Build',
- target: '${{ matrix.target }}',
- status: 'success',
- pr_number: ${{ github.event.pull_request.number }},
- commit_sha: '${{ github.event.pull_request.head.sha }}',
- run_id: ${{ github.run_id }},
- run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
- }
- });
-
- - name: ๐ Notify artifact comment workflow (failure)
- if: failure() && github.event_name == 'pull_request'
- uses: actions/github-script@v8
- with:
- script: |
- await github.rest.repos.createDispatchEvent({
- owner: context.repo.owner,
- repo: context.repo.repo,
- event_type: 'build-failed',
- client_payload: {
- workflow_name: 'iOS IPA Build',
- target: '${{ matrix.target }}',
- status: 'failure',
- pr_number: ${{ github.event.pull_request.number }},
- commit_sha: '${{ github.event.pull_request.head.sha }}',
- run_id: ${{ github.run_id }},
- run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
- }
- });
From fb0a70690eed20d82950d50097b214b9269b2d26 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 01:20:11 +0200
Subject: [PATCH 23/27] refactor: consolidate build workflows into unified app
builder
Merges separate Android and iOS build workflows into a single "Build Apps" workflow to reduce duplication and simplify maintenance.
Updates the artifact comment workflow to handle both the new unified workflow and legacy separate workflows for backward compatibility during transition.
Removes redundant workflow files while preserving all existing functionality.
---
.github/workflows/artifact-comment.yml | 54 ++---
.github/workflows/build-android.yml | 93 --------
.github/workflows/build-apps.yml | 280 +++++++++++++++++++++++++
.github/workflows/build-ios.yml | 95 ---------
4 files changed, 309 insertions(+), 213 deletions(-)
delete mode 100644 .github/workflows/build-android.yml
create mode 100644 .github/workflows/build-apps.yml
delete mode 100644 .github/workflows/build-ios.yml
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index c4773b62..08710fe8 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -10,8 +10,7 @@ on:
types: [opened, synchronize, reopened]
workflow_run: # Triggered when build workflows complete
workflows:
- - "๐ค Android APK Build (Phone + TV)"
- - "๐ค iOS IPA Build (Phone + TV)"
+ - "๐๏ธ Build Apps"
types:
- completed
@@ -116,6 +115,7 @@ jobs:
// Filter for build workflows only and sort by creation time (most recent first)
const buildRuns = workflowRuns.workflow_runs
.filter(run =>
+ run.name.includes('Build Apps') ||
run.name.includes('Android APK Build') ||
run.name.includes('iOS IPA Build')
)
@@ -132,57 +132,61 @@ jobs:
let allArtifacts = [];
let buildStatuses = {};
- // Get the most recent run for each workflow type
+ // Get the most recent run for the unified apps workflow
+ const latestAppsRun = buildRuns.find(run => run.name.includes('Build Apps'));
const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build'));
const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build'));
- // Store status for each workflow type
- if (latestAndroidRun) {
+ // Store status for unified workflow (preferred) or fallback to separate workflows
+ const appsWorkflowRun = latestAppsRun || latestAndroidRun;
+ if (appsWorkflowRun) {
buildStatuses['Android'] = {
- name: latestAndroidRun.name,
- status: latestAndroidRun.status,
- conclusion: latestAndroidRun.conclusion,
- url: latestAndroidRun.html_url,
- runId: latestAndroidRun.id,
- created_at: latestAndroidRun.created_at
+ name: appsWorkflowRun.name,
+ status: appsWorkflowRun.status,
+ conclusion: appsWorkflowRun.conclusion,
+ url: appsWorkflowRun.html_url,
+ runId: appsWorkflowRun.id,
+ created_at: appsWorkflowRun.created_at
};
// Collect artifacts if completed successfully
- if (latestAndroidRun.conclusion === 'success') {
+ if (appsWorkflowRun.conclusion === 'success') {
try {
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
- run_id: latestAndroidRun.id
+ run_id: appsWorkflowRun.id
});
allArtifacts.push(...artifacts.artifacts);
} catch (error) {
- console.log(`Failed to get Android artifacts for run ${latestAndroidRun.id}:`, error.message);
+ console.log(`Failed to get apps artifacts for run ${appsWorkflowRun.id}:`, error.message);
}
}
}
- if (latestIOSRun) {
+ // For iOS, use the same workflow run (since it's all in one now) or fallback to separate
+ const iosWorkflowRun = latestAppsRun || latestIOSRun;
+ if (iosWorkflowRun) {
buildStatuses['iOS'] = {
- name: latestIOSRun.name,
- status: latestIOSRun.status,
- conclusion: latestIOSRun.conclusion,
- url: latestIOSRun.html_url,
- runId: latestIOSRun.id,
- created_at: latestIOSRun.created_at
+ name: iosWorkflowRun.name,
+ status: iosWorkflowRun.status,
+ conclusion: iosWorkflowRun.conclusion,
+ url: iosWorkflowRun.html_url,
+ runId: iosWorkflowRun.id,
+ created_at: iosWorkflowRun.created_at
};
- // Collect artifacts if completed successfully
- if (latestIOSRun.conclusion === 'success') {
+ // Only collect artifacts if not already collected from apps workflow
+ if (!latestAppsRun && iosWorkflowRun.conclusion === 'success') {
try {
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
- run_id: latestIOSRun.id
+ run_id: iosWorkflowRun.id
});
allArtifacts.push(...artifacts.artifacts);
} catch (error) {
- console.log(`Failed to get iOS artifacts for run ${latestIOSRun.id}:`, error.message);
+ console.log(`Failed to get iOS artifacts for run ${iosWorkflowRun.id}:`, error.message);
}
}
}
diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml
deleted file mode 100644
index bc9b9ea1..00000000
--- a/.github/workflows/build-android.yml
+++ /dev/null
@@ -1,93 +0,0 @@
-name: ๐ค Android APK Build (Phone + TV)
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-on:
- workflow_dispatch:
- pull_request:
- branches: [develop, master]
- push:
- branches: [develop, master]
-
-jobs:
- build-android:
- if: (!contains(github.event.head_commit.message, '[skip ci]'))
- runs-on: ubuntu-24.04
- name: ๐๏ธ Build Android APK
- permissions:
- contents: read
-
- strategy:
- fail-fast: false
- matrix:
- target: [phone, tv]
-
- 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 }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
- restore-keys: |
- ${{ runner.os }}-${{ runner.arch }}-bun-develop
- ${{ runner.os }}-bun-develop
-
- - name: ๐ฆ Install dependencies and reload submodules
- run: |
- bun install --frozen-lockfile
- bun run submodule-reload
-
- - name: ๐พ Cache Gradle global
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }}
- restore-keys: ${{ runner.os }}-gradle-develop
-
- - name: ๐ ๏ธ Generate project files
- run: |
- if [ "${{ matrix.target }}" = "tv" ]; then
- bun run prebuild:tv
- else
- bun run prebuild
- fi
-
- - name: ๐พ Cache project Gradle (.gradle)
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.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
-
- - name: ๐ Build APK
- env:
- EXPO_TV: ${{ matrix.target == 'tv' && 1 || 0 }}
- run: bun run build:android:local
-
- - name: ๐
Set date tag
- run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
-
- - name: ๐ค Upload APK artifact
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
- with:
- name: streamyfin-android-${{ matrix.target }}-apk-${{ env.DATE_TAG }}
- path: |
- android/app/build/outputs/apk/release/*.apk
- retention-days: 7
diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml
new file mode 100644
index 00000000..ad770414
--- /dev/null
+++ b/.github/workflows/build-apps.yml
@@ -0,0 +1,280 @@
+name: ๐๏ธ Build Apps
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: [develop, master]
+ push:
+ branches: [develop, master]
+
+jobs:
+ build-android-phone:
+ if: (!contains(github.event.head_commit.message, '[skip ci]'))
+ runs-on: ubuntu-24.04
+ name: ๐ค Build Android APK (Phone)
+ 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 }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ runner.arch }}-bun-develop
+ ${{ runner.os }}-bun-develop
+
+ - name: ๐ฆ Install dependencies and reload submodules
+ run: |
+ bun install --frozen-lockfile
+ bun run submodule-reload
+
+ - name: ๐พ Cache Gradle global
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: ๐ ๏ธ Generate project files
+ run: bun run prebuild
+
+ - name: ๐พ Cache project Gradle (.gradle)
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.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
+
+ - name: ๐ Build APK
+ env:
+ EXPO_TV: 0
+ run: bun run build:android:local
+
+ - name: ๐
Set date tag
+ run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
+
+ - name: ๐ค Upload APK artifact
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: streamyfin-android-phone-apk-${{ env.DATE_TAG }}
+ path: |
+ android/app/build/outputs/apk/release/*.apk
+ retention-days: 7
+
+ build-android-tv:
+ if: (!contains(github.event.head_commit.message, '[skip ci]'))
+ runs-on: ubuntu-24.04
+ name: ๐ค Build Android APK (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 }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-${{ runner.arch }}-bun-develop
+ ${{ runner.os }}-bun-develop
+
+ - name: ๐ฆ Install dependencies and reload submodules
+ run: |
+ bun install --frozen-lockfile
+ bun run submodule-reload
+
+ - name: ๐พ Cache Gradle global
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: ๐ ๏ธ Generate project files
+ run: bun run prebuild:tv
+
+ - name: ๐พ Cache project Gradle (.gradle)
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.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
+
+ - name: ๐ Build APK
+ env:
+ EXPO_TV: 1
+ run: bun run build:android:local
+
+ - name: ๐
Set date tag
+ run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV
+
+ - name: ๐ค Upload APK artifact
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: streamyfin-android-tv-apk-${{ env.DATE_TAG }}
+ path: |
+ android/app/build/outputs/apk/release/*.apk
+ retention-days: 7
+
+ 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-15
+ name: ๐ Build iOS IPA (Phone)
+ 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
+
+ - name: ๐๏ธ Setup EAS
+ uses: expo/expo-github-action@main
+ with:
+ eas-version: latest
+ token: ${{ secrets.EXPO_TOKEN }}
+ eas-cache: true
+
+ - name: โ๏ธ Ensure iOS SDKs installed
+ run: xcodebuild -downloadPlatform iOS
+
+ - name: ๐ Build iOS app
+ env:
+ EXPO_TV: 0
+ 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-phone-ipa-${{ env.DATE_TAG }}
+ 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-15
+ # 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 EAS
+ # uses: expo/expo-github-action@main
+ # with:
+ # eas-version: latest
+ # token: ${{ secrets.EXPO_TOKEN }}
+ # eas-cache: true
+ #
+ # - name: โ๏ธ Ensure tvOS SDKs installed
+ # run: xcodebuild -downloadPlatform tvOS
+ #
+ # - 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
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
deleted file mode 100644
index 76e4a4d2..00000000
--- a/.github/workflows/build-ios.yml
+++ /dev/null
@@ -1,95 +0,0 @@
-name: ๐ค iOS IPA Build (Phone + TV)
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: true
-
-on:
- workflow_dispatch:
- pull_request:
- branches: [develop, master]
- paths-ignore:
- - "*.md"
- push:
- branches: [develop, master]
- paths-ignore:
- - "*.md"
-
-jobs:
- build-ios:
- 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-15
- name: ๐๏ธ Build iOS IPA
- permissions:
- contents: read
-
- strategy:
- fail-fast: false
- matrix:
- target: [phone]
- # target: [phone, tv]
-
- 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: |
- if [ "${{ matrix.target }}" = "tv" ]; then
- bun run prebuild:tv
- else
- bun run prebuild
- fi
-
- - name: ๐๏ธ Setup EAS
- uses: expo/expo-github-action@main
- with:
- eas-version: latest
- token: ${{ secrets.EXPO_TOKEN }}
- eas-cache: true
-
- - name: โ๏ธ Ensure iOS/tvOS SDKs installed
- run: |
- if [ "${{ matrix.target }}" = "tv" ]; then
- xcodebuild -downloadPlatform tvOS
- else
- xcodebuild -downloadPlatform iOS
- fi
-
- - name: ๐ Build iOS app
- env:
- EXPO_TV: ${{ matrix.target == 'tv' && 1 || 0 }}
- 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-${{ matrix.target }}-ipa-${{ env.DATE_TAG }}
- path: build-*.ipa
- retention-days: 7
From 312a59c5b0a1369b7aa27374284d2cdf9a3fa0a9 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 01:42:17 +0200
Subject: [PATCH 24/27] refactor(ci): improve build workflow handling and
artifact collection logic
Restructures the GitHub Actions workflow to better handle consolidated vs separate build workflows.
Changes the artifact collection to trigger on workflow completion rather than just success, improving visibility of failed builds.
Adds explicit fallback logic for backward compatibility with separate Android and iOS workflows.
Introduces artifact pattern matching for more reliable build target identification and adds special handling to disable iOS TV builds.
Enhances debugging output to show which workflow type is being used and lists all discovered artifacts.
---
.github/workflows/artifact-comment.yml | 133 +++++++++++++++++--------
1 file changed, 90 insertions(+), 43 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 08710fe8..0e9eea6c 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -137,62 +137,105 @@ jobs:
const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build'));
const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build'));
- // Store status for unified workflow (preferred) or fallback to separate workflows
- const appsWorkflowRun = latestAppsRun || latestAndroidRun;
- if (appsWorkflowRun) {
+ // For the consolidated workflow, both Android and iOS share the same run
+ if (latestAppsRun) {
+ // Both platforms use same workflow run status
buildStatuses['Android'] = {
- name: appsWorkflowRun.name,
- status: appsWorkflowRun.status,
- conclusion: appsWorkflowRun.conclusion,
- url: appsWorkflowRun.html_url,
- runId: appsWorkflowRun.id,
- created_at: appsWorkflowRun.created_at
+ name: latestAppsRun.name,
+ status: latestAppsRun.status,
+ conclusion: latestAppsRun.conclusion,
+ url: latestAppsRun.html_url,
+ runId: latestAppsRun.id,
+ created_at: latestAppsRun.created_at
};
- // Collect artifacts if completed successfully
- if (appsWorkflowRun.conclusion === 'success') {
+ buildStatuses['iOS'] = {
+ name: latestAppsRun.name,
+ status: latestAppsRun.status,
+ conclusion: latestAppsRun.conclusion,
+ url: latestAppsRun.html_url,
+ runId: latestAppsRun.id,
+ created_at: latestAppsRun.created_at
+ };
+
+ // Collect artifacts if workflow has completed (regardless of success/failure)
+ if (latestAppsRun.status === 'completed') {
try {
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
- run_id: appsWorkflowRun.id
+ run_id: latestAppsRun.id
});
allArtifacts.push(...artifacts.artifacts);
} catch (error) {
- console.log(`Failed to get apps artifacts for run ${appsWorkflowRun.id}:`, error.message);
+ console.log(`Failed to get apps artifacts for run ${latestAppsRun.id}:`, error.message);
+ }
+ }
+ } else {
+ // Fallback to separate workflows (for backward compatibility)
+ if (latestAndroidRun) {
+ buildStatuses['Android'] = {
+ name: latestAndroidRun.name,
+ status: latestAndroidRun.status,
+ conclusion: latestAndroidRun.conclusion,
+ url: latestAndroidRun.html_url,
+ runId: latestAndroidRun.id,
+ created_at: latestAndroidRun.created_at
+ };
+
+ if (latestAndroidRun.conclusion === 'success') {
+ try {
+ const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: latestAndroidRun.id
+ });
+ allArtifacts.push(...artifacts.artifacts);
+ } catch (error) {
+ console.log(`Failed to get Android artifacts for run ${latestAndroidRun.id}:`, error.message);
+ }
}
}
- }
-
- // For iOS, use the same workflow run (since it's all in one now) or fallback to separate
- const iosWorkflowRun = latestAppsRun || latestIOSRun;
- if (iosWorkflowRun) {
- buildStatuses['iOS'] = {
- name: iosWorkflowRun.name,
- status: iosWorkflowRun.status,
- conclusion: iosWorkflowRun.conclusion,
- url: iosWorkflowRun.html_url,
- runId: iosWorkflowRun.id,
- created_at: iosWorkflowRun.created_at
- };
- // Only collect artifacts if not already collected from apps workflow
- if (!latestAppsRun && iosWorkflowRun.conclusion === 'success') {
- try {
- const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: iosWorkflowRun.id
- });
- allArtifacts.push(...artifacts.artifacts);
- } catch (error) {
- console.log(`Failed to get iOS artifacts for run ${iosWorkflowRun.id}:`, error.message);
+ if (latestIOSRun) {
+ buildStatuses['iOS'] = {
+ name: latestIOSRun.name,
+ status: latestIOSRun.status,
+ conclusion: latestIOSRun.conclusion,
+ url: latestIOSRun.html_url,
+ runId: latestIOSRun.id,
+ created_at: latestIOSRun.created_at
+ };
+
+ if (latestIOSRun.conclusion === 'success') {
+ try {
+ const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: latestIOSRun.id
+ });
+ allArtifacts.push(...artifacts.artifacts);
+ } catch (error) {
+ console.log(`Failed to get iOS artifacts for run ${latestIOSRun.id}:`, error.message);
+ }
}
}
}
console.log(`Collected ${allArtifacts.length} total artifacts from all builds`);
+ // Debug: Show which workflow we're using and its status
+ if (latestAppsRun) {
+ console.log(`Using consolidated workflow: ${latestAppsRun.name} (${latestAppsRun.status}/${latestAppsRun.conclusion})`);
+ } else {
+ console.log(`Using separate workflows - Android: ${latestAndroidRun?.name || 'none'}, iOS: ${latestIOSRun?.name || 'none'}`);
+ }
+
+ // Debug: List all artifacts found
+ allArtifacts.forEach(artifact => {
+ console.log(`- Artifact: ${artifact.name} (from run ${artifact.workflow_run.id})`);
+ });
+
// 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
@@ -202,10 +245,10 @@ jobs:
// Process each expected build target individually
const buildTargets = [
- { name: 'Android Phone', platform: '๐ค', device: '๐ฑ', workflowType: 'Android', target: 'phone' },
- { name: 'Android TV', platform: '๐ค', device: '๐บ', workflowType: 'Android', target: 'tv' },
- { name: 'iOS Phone', platform: '๐', device: '๐ฑ', workflowType: 'iOS', target: 'phone' },
- { name: 'iOS TV', platform: '๐', device: '๐บ', workflowType: 'iOS', target: 'tv' }
+ { name: 'Android Phone', platform: '๐ค', device: '๐ฑ', workflowType: 'Android', target: 'phone', artifactPattern: /android.*phone/i },
+ { name: 'Android TV', platform: '๐ค', device: '๐บ', workflowType: 'Android', target: 'tv', artifactPattern: /android.*tv/i },
+ { name: 'iOS Phone', platform: '๐', device: '๐ฑ', workflowType: 'iOS', target: 'phone', artifactPattern: /ios.*phone/i },
+ { name: 'iOS TV', platform: '๐', device: '๐บ', workflowType: 'iOS', target: 'tv', artifactPattern: /ios.*tv/i }
];
for (const target of buildTargets) {
@@ -214,13 +257,17 @@ jobs:
// Find matching artifact
const matchingArtifact = allArtifacts.find(artifact =>
- target.pattern.test(artifact.name)
+ target.artifactPattern.test(artifact.name)
);
let status = 'โณ Pending';
let downloadLink = '*Waiting for build...*';
- if (matchingStatus) {
+ // Special case for iOS TV - show as disabled
+ if (target.name === 'iOS TV') {
+ status = '๐ค Disabled';
+ downloadLink = '*Disabled for now*';
+ } else if (matchingStatus) {
if (matchingStatus.conclusion === 'success' && matchingArtifact) {
status = 'โ
Complete';
const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`;
From 0a41962ddfd0f9c4f2553ba4b906febe28bbf90f Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 01:49:38 +0200
Subject: [PATCH 25/27] fix: improve build status handling and artifact links
Replaces nightly.link with direct GitHub artifact URLs for better reliability.
Adds handling for cancelled builds and edge cases where workflows complete
but artifacts aren't immediately available or conclusions are pending.
Improves status messages to provide more detailed information for
unexpected build states.
---
.github/workflows/artifact-comment.yml | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 0e9eea6c..61a94b0a 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -270,22 +270,33 @@ jobs:
} else if (matchingStatus) {
if (matchingStatus.conclusion === 'success' && matchingArtifact) {
status = 'โ
Complete';
- const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`;
+ 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}](${nightlyLink})`;
+ downloadLink = `[๐ฅ Download ${fileType}](${directLink})`;
} else if (matchingStatus.conclusion === 'failure') {
status = `โ [Failed](${matchingStatus.url})`;
downloadLink = '*Build failed*';
+ } else if (matchingStatus.conclusion === 'cancelled') {
+ status = `โช [Cancelled](${matchingStatus.url})`;
+ downloadLink = '*Build cancelled*';
} else if (matchingStatus.status === 'in_progress') {
status = `๐ [Building...](${matchingStatus.url})`;
downloadLink = '*Build in progress...*';
} else if (matchingStatus.status === 'queued') {
status = `โณ [Queued](${matchingStatus.url})`;
downloadLink = '*Waiting to start...*';
+ } else if (matchingStatus.status === 'completed' && !matchingStatus.conclusion) {
+ // Workflow completed but conclusion not yet available (rare edge case)
+ status = `๐ [Finishing...](${matchingStatus.url})`;
+ downloadLink = '*Finalizing build...*';
+ } else if (matchingStatus.status === 'completed' && matchingStatus.conclusion === 'success' && !matchingArtifact) {
+ // Build succeeded but artifacts not yet available
+ status = `โณ [Processing artifacts...](${matchingStatus.url})`;
+ downloadLink = '*Preparing download...*';
} else {
- // Show any other status with timestamp for debugging
- status = `๐ [${matchingStatus.status}](${matchingStatus.url})`;
- downloadLink = `*Status: ${matchingStatus.status}*`;
+ // Fallback for any unexpected states
+ status = `โ [${matchingStatus.status}/${matchingStatus.conclusion || 'pending'}](${matchingStatus.url})`;
+ downloadLink = `*Status: ${matchingStatus.status}, Conclusion: ${matchingStatus.conclusion || 'pending'}*`;
}
}
From 71a3c5e92bfbea1e84cf8c901544a0a125b3995f Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 01:57:18 +0200
Subject: [PATCH 26/27] feat: improve GitHub workflow status tracking
Enhances artifact comment workflow to provide more accurate build status reporting by tracking individual job statuses within consolidated workflows rather than using workflow-level status.
Excludes cancelled workflow runs from consideration and prioritizes active runs over completed ones when determining build status.
Maps specific job names to build targets (Android Phone, Android TV, iOS Phone) to provide granular status information for each platform and device combination.
Improves artifact collection logic to gather artifacts when any job completes successfully, not just when entire workflow completes.
---
.github/workflows/artifact-comment.yml | 129 ++++++++++++++++++-------
1 file changed, 94 insertions(+), 35 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 61a94b0a..0a8cc576 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -112,54 +112,113 @@ jobs:
per_page: 30
});
- // Filter for build workflows only and sort by creation time (most recent first)
+ // Filter for build workflows only, exclude cancelled runs, and sort by creation time (most recent first)
const buildRuns = workflowRuns.workflow_runs
.filter(run =>
- run.name.includes('Build Apps') ||
+ (run.name.includes('Build Apps') ||
run.name.includes('Android APK Build') ||
- run.name.includes('iOS IPA Build')
+ run.name.includes('iOS IPA Build')) &&
+ run.conclusion !== 'cancelled' // Ignore cancelled runs
)
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
- console.log(`Found ${buildRuns.length} build workflow runs for this commit`);
+ console.log(`Found ${buildRuns.length} non-cancelled build workflow runs for this commit`);
// Log current status of each build for debugging
buildRuns.forEach(run => {
console.log(`- ${run.name}: ${run.status} (${run.conclusion || 'no conclusion yet'}) - Created: ${run.created_at}`);
});
- // Collect artifacts and statuses from builds - get most recent run for each workflow type
+ // Collect artifacts and statuses from builds - prioritize active runs over completed ones
let allArtifacts = [];
let buildStatuses = {};
- // Get the most recent run for the unified apps workflow
- const latestAppsRun = buildRuns.find(run => run.name.includes('Build Apps'));
- const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build'));
- const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build'));
+ // Get the most relevant run for each workflow type (prioritize in_progress over completed)
+ const findBestRun = (nameFilter) => {
+ const matchingRuns = buildRuns.filter(run => run.name.includes(nameFilter));
+ // First try to find an in-progress run
+ const inProgressRun = matchingRuns.find(run => run.status === 'in_progress');
+ if (inProgressRun) return inProgressRun;
+ // Then try to find a queued run
+ const queuedRun = matchingRuns.find(run => run.status === 'queued');
+ if (queuedRun) return queuedRun;
+ // Finally fall back to most recent completed run
+ return matchingRuns[0]; // Already sorted by most recent first
+ };
- // For the consolidated workflow, both Android and iOS share the same run
+ const latestAppsRun = findBestRun('Build Apps');
+ const latestAndroidRun = findBestRun('Android APK Build');
+ const latestIOSRun = findBestRun('iOS IPA Build');
+
+ // For the consolidated workflow, get individual job statuses
if (latestAppsRun) {
- // Both platforms use same workflow run status
- buildStatuses['Android'] = {
- name: latestAppsRun.name,
- status: latestAppsRun.status,
- conclusion: latestAppsRun.conclusion,
- url: latestAppsRun.html_url,
- runId: latestAppsRun.id,
- created_at: latestAppsRun.created_at
- };
+ console.log(`Getting individual job statuses for run ${latestAppsRun.id}`);
- buildStatuses['iOS'] = {
- name: latestAppsRun.name,
- status: latestAppsRun.status,
- conclusion: latestAppsRun.conclusion,
- url: latestAppsRun.html_url,
- runId: latestAppsRun.id,
- created_at: latestAppsRun.created_at
- };
+ try {
+ // Get all jobs for this workflow run
+ const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: latestAppsRun.id
+ });
+
+ console.log(`Found ${jobs.jobs.length} jobs in workflow run`);
+ jobs.jobs.forEach(job => {
+ console.log(`- Job: ${job.name} | Status: ${job.status} | Conclusion: ${job.conclusion || 'none'}`);
+ });
+
+ // Map job names to our build targets
+ 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']
+ };
+
+ // 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)
+ );
+
+ if (job) {
+ buildStatuses[platform] = {
+ name: job.name,
+ status: job.status,
+ conclusion: job.conclusion,
+ url: job.html_url,
+ runId: latestAppsRun.id,
+ created_at: job.started_at || latestAppsRun.created_at
+ };
+ console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`);
+ } else {
+ console.log(`No job found for ${platform}, using workflow status as fallback`);
+ buildStatuses[platform] = {
+ name: latestAppsRun.name,
+ status: latestAppsRun.status,
+ conclusion: latestAppsRun.conclusion,
+ url: latestAppsRun.html_url,
+ runId: latestAppsRun.id,
+ created_at: latestAppsRun.created_at
+ };
+ }
+ }
+
+ } catch (error) {
+ console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message);
+ // Fallback to workflow-level status
+ buildStatuses['Android Phone'] = buildStatuses['Android TV'] = buildStatuses['iOS Phone'] = {
+ name: latestAppsRun.name,
+ status: latestAppsRun.status,
+ conclusion: latestAppsRun.conclusion,
+ url: latestAppsRun.html_url,
+ runId: latestAppsRun.id,
+ created_at: latestAppsRun.created_at
+ };
+ }
- // Collect artifacts if workflow has completed (regardless of success/failure)
- if (latestAppsRun.status === 'completed') {
+ // Collect artifacts if any job has completed successfully
+ if (latestAppsRun.status === 'completed' ||
+ Object.values(buildStatuses).some(status => status.conclusion === 'success')) {
try {
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
@@ -245,15 +304,15 @@ jobs:
// Process each expected build target individually
const buildTargets = [
- { name: 'Android Phone', platform: '๐ค', device: '๐ฑ', workflowType: 'Android', target: 'phone', artifactPattern: /android.*phone/i },
- { name: 'Android TV', platform: '๐ค', device: '๐บ', workflowType: 'Android', target: 'tv', artifactPattern: /android.*tv/i },
- { name: 'iOS Phone', platform: '๐', device: '๐ฑ', workflowType: 'iOS', target: 'phone', artifactPattern: /ios.*phone/i },
- { name: 'iOS TV', platform: '๐', device: '๐บ', workflowType: 'iOS', target: 'tv', artifactPattern: /ios.*tv/i }
+ { 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 }
];
for (const target of buildTargets) {
- // Find matching workflow status
- const matchingStatus = buildStatuses[target.workflowType];
+ // Find matching job status directly
+ const matchingStatus = buildStatuses[target.statusKey];
// Find matching artifact
const matchingArtifact = allArtifacts.find(artifact =>
From c6ad06b0843fb90d7e06f3c443aee0056888fbc3 Mon Sep 17 00:00:00 2001
From: Uruk
Date: Tue, 30 Sep 2025 02:15:25 +0200
Subject: [PATCH 27/27] fix: improve workflow run detection for cancelled jobs
Enhances the artifact comment workflow to better handle cancelled workflow runs by checking individual job statuses instead of dismissing entire workflows.
Previously, workflows marked as cancelled at the top level were completely ignored, even if some jobs within them were still running or completed successfully.
Now prioritizes active jobs over cancelled ones and validates that at least one job is actually running before processing a workflow run, preventing false negatives while still filtering out truly cancelled workflows.
---
.github/workflows/artifact-comment.yml | 38 ++++++++++++++++++++++----
1 file changed, 32 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml
index 0a8cc576..d032dd67 100644
--- a/.github/workflows/artifact-comment.yml
+++ b/.github/workflows/artifact-comment.yml
@@ -112,13 +112,12 @@ jobs:
per_page: 30
});
- // Filter for build workflows only, exclude cancelled runs, and sort by creation time (most recent first)
+ // Filter for build workflows only, include active runs even if marked as cancelled
const buildRuns = workflowRuns.workflow_runs
.filter(run =>
(run.name.includes('Build Apps') ||
run.name.includes('Android APK Build') ||
- run.name.includes('iOS IPA Build')) &&
- run.conclusion !== 'cancelled' // Ignore cancelled runs
+ run.name.includes('iOS IPA Build'))
)
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
@@ -133,16 +132,28 @@ jobs:
let allArtifacts = [];
let buildStatuses = {};
- // Get the most relevant run for each workflow type (prioritize in_progress over completed)
+ // Get the most relevant run for each workflow type (prioritize active over cancelled)
const findBestRun = (nameFilter) => {
const matchingRuns = buildRuns.filter(run => run.name.includes(nameFilter));
+
// First try to find an in-progress run
const inProgressRun = matchingRuns.find(run => run.status === 'in_progress');
if (inProgressRun) return inProgressRun;
+
// Then try to find a queued run
const queuedRun = matchingRuns.find(run => run.status === 'queued');
if (queuedRun) return queuedRun;
- // Finally fall back to most recent completed run
+
+ // Check if the workflow is completed but has non-cancelled jobs
+ const completedRuns = matchingRuns.filter(run => run.status === 'completed');
+ for (const run of completedRuns) {
+ // We'll check individual jobs later to see if they're actually running
+ if (run.conclusion !== 'cancelled') {
+ return run;
+ }
+ }
+
+ // Finally fall back to most recent run (even if cancelled at workflow level)
return matchingRuns[0]; // Already sorted by most recent first
};
@@ -152,7 +163,7 @@ jobs:
// For the consolidated workflow, get individual job statuses
if (latestAppsRun) {
- console.log(`Getting individual job statuses for run ${latestAppsRun.id}`);
+ console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
try {
// Get all jobs for this workflow run
@@ -167,6 +178,21 @@ jobs:
console.log(`- Job: ${job.name} | Status: ${job.status} | Conclusion: ${job.conclusion || 'none'}`);
});
+ // Check if we have any actually running jobs (not cancelled)
+ const activeJobs = jobs.jobs.filter(job =>
+ job.status === 'in_progress' ||
+ job.status === 'queued' ||
+ (job.status === 'completed' && job.conclusion !== 'cancelled')
+ );
+
+ console.log(`Found ${activeJobs.length} active (non-cancelled) jobs out of ${jobs.jobs.length} total jobs`);
+
+ // If no jobs are actually running, skip this workflow
+ if (activeJobs.length === 0 && latestAppsRun.conclusion === 'cancelled') {
+ console.log('All jobs are cancelled, skipping this workflow run');
+ return; // Exit early
+ }
+
// Map job names to our build targets
const jobMappings = {
'Android Phone': ['๐ค Build Android APK (Phone)', 'build-android-phone'],