mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-16 19:00:28 +01:00
Compare commits
1 Commits
develop
...
ci/artifac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
132d378346 |
132
.github/workflows/artifact-comment.yml
vendored
132
.github/workflows/artifact-comment.yml
vendored
@@ -144,7 +144,7 @@ jobs:
|
|||||||
)
|
)
|
||||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
|
|
||||||
console.log(`Found ${buildRuns.length} non-cancelled build workflow runs for this commit`);
|
console.log(`Found ${buildRuns.length} build workflow runs for this commit`);
|
||||||
|
|
||||||
// Log current status of each build for debugging
|
// Log current status of each build for debugging
|
||||||
buildRuns.forEach(run => {
|
buildRuns.forEach(run => {
|
||||||
@@ -184,21 +184,35 @@ jobs:
|
|||||||
const latestAndroidRun = findBestRun('Android APK Build');
|
const latestAndroidRun = findBestRun('Android APK Build');
|
||||||
const latestIOSRun = findBestRun('iOS IPA Build');
|
const latestIOSRun = findBestRun('iOS IPA Build');
|
||||||
|
|
||||||
|
// Map our build targets to their job display names. Exact name is
|
||||||
|
// tried first so a signed target never collides with its
|
||||||
|
// "(Unsigned)" sibling (whose name contains the signed name).
|
||||||
|
const jobMappings = {
|
||||||
|
'Android Phone': ['🤖 Build Android APK (Phone)'],
|
||||||
|
'Android TV': ['🤖 Build Android APK (TV)'],
|
||||||
|
'iOS': ['🍎 Build iOS IPA (Phone)'],
|
||||||
|
'iOS Unsigned': ['🍎 Build iOS IPA (Phone - Unsigned)'],
|
||||||
|
'tvOS': ['🍎 Build tvOS IPA'],
|
||||||
|
'tvOS Unsigned': ['🍎 Build tvOS IPA (Unsigned)']
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prefer an exact name match over a substring match so
|
||||||
|
// '...(Phone)' doesn't swallow '...(Phone - Unsigned)'.
|
||||||
|
const findJobForTarget = (jobs, jobNames) =>
|
||||||
|
jobs.find(j => jobNames.some(name => j.name === name)) ||
|
||||||
|
jobs.find(j => jobNames.some(name => j.name.includes(name)));
|
||||||
|
|
||||||
|
// Format a millisecond duration as "Xm Ys".
|
||||||
|
const fmtDuration = (ms) => {
|
||||||
|
const min = Math.floor(ms / 60000);
|
||||||
|
const sec = Math.floor((ms % 60000) / 1000);
|
||||||
|
return `${min}m ${sec}s`;
|
||||||
|
};
|
||||||
|
|
||||||
// For the consolidated workflow, get individual job statuses
|
// For the consolidated workflow, get individual job statuses
|
||||||
if (latestAppsRun) {
|
if (latestAppsRun) {
|
||||||
console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
|
console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`);
|
||||||
|
|
||||||
// Map job names to our build targets. Declared outside the try so
|
|
||||||
// the catch fallback can reuse the same keys.
|
|
||||||
const jobMappings = {
|
|
||||||
'Android Phone': ['🤖 Build Android APK (Phone)', 'build-android-phone'],
|
|
||||||
'Android TV': ['🤖 Build Android APK (TV)', 'build-android-tv'],
|
|
||||||
'iOS': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone'],
|
|
||||||
'iOS Unsigned': ['🍎 Build iOS IPA (Phone - Unsigned)', 'build-ios-phone-unsigned'],
|
|
||||||
'tvOS': ['🍎 Build tvOS IPA', 'build-ios-tv'],
|
|
||||||
'tvOS Unsigned': ['🍎 Build tvOS IPA (Unsigned)', 'build-ios-tv-unsigned']
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all jobs for this workflow run
|
// Get all jobs for this workflow run
|
||||||
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
||||||
@@ -229,10 +243,8 @@ jobs:
|
|||||||
|
|
||||||
// Create individual status for each job
|
// Create individual status for each job
|
||||||
for (const [platform, jobNames] of Object.entries(jobMappings)) {
|
for (const [platform, jobNames] of Object.entries(jobMappings)) {
|
||||||
const job = jobs.jobs.find(j =>
|
const job = findJobForTarget(jobs.jobs, jobNames);
|
||||||
jobNames.some(name => j.name.includes(name) || j.name === name)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (job) {
|
if (job) {
|
||||||
buildStatuses[platform] = {
|
buildStatuses[platform] = {
|
||||||
name: job.name,
|
name: job.name,
|
||||||
@@ -358,6 +370,43 @@ jobs:
|
|||||||
console.log(`- Artifact: ${artifact.name} (from run ${artifact.workflow_run.id})`);
|
console.log(`- Artifact: ${artifact.name} (from run ${artifact.workflow_run.id})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pull per-job durations from the latest successful develop build so
|
||||||
|
// in-progress / queued targets can show a realistic ETA instead of
|
||||||
|
// an open-ended spinner. Best-effort: any failure just drops the ETA.
|
||||||
|
let referenceDurations = {};
|
||||||
|
try {
|
||||||
|
const { data: devRuns } = await github.rest.actions.listWorkflowRuns({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
workflow_id: 'build-apps.yml',
|
||||||
|
branch: 'develop',
|
||||||
|
status: 'success',
|
||||||
|
per_page: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
if (devRuns.workflow_runs.length > 0) {
|
||||||
|
const refRun = devRuns.workflow_runs[0];
|
||||||
|
const { data: refJobs } = await github.rest.actions.listJobsForWorkflowRun({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: refRun.id
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [platform, jobNames] of Object.entries(jobMappings)) {
|
||||||
|
const job = findJobForTarget(refJobs.jobs, jobNames);
|
||||||
|
if (job && job.conclusion === 'success' && job.started_at && job.completed_at) {
|
||||||
|
referenceDurations[platform] = new Date(job.completed_at) - new Date(job.started_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Reference durations from develop run ${refRun.id}:`,
|
||||||
|
Object.fromEntries(Object.entries(referenceDurations).map(([k, v]) => [k, fmtDuration(v)])));
|
||||||
|
} else {
|
||||||
|
console.log('No successful develop build found for ETA reference');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to fetch develop reference durations:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Build comment body with progressive status for individual builds
|
// 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**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\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
|
||||||
@@ -369,9 +418,9 @@ jobs:
|
|||||||
const buildTargets = [
|
const buildTargets = [
|
||||||
{ name: 'Android Phone', platform: '🤖', device: '📱 Phone', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
|
{ name: 'Android Phone', platform: '🤖', device: '📱 Phone', statusKey: 'Android Phone', artifactPattern: /android.*phone/i },
|
||||||
{ name: 'Android TV', platform: '🤖', device: '📺 TV', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
|
{ name: 'Android TV', platform: '🤖', device: '📺 TV', statusKey: 'Android TV', artifactPattern: /android.*tv/i },
|
||||||
{ name: 'iOS', platform: '🍎', device: '📱 Phone', statusKey: 'iOS', artifactPattern: /ios.*phone.*ipa(?!.*unsigned)/i },
|
{ name: 'iOS', platform: '🍎', device: '📱 Phone', statusKey: 'iOS', artifactPattern: /^(?!.*unsigned).*ios.*phone.*ipa/i },
|
||||||
{ name: 'iOS Unsigned', platform: '🍎', device: '📱 Phone Unsigned', statusKey: 'iOS Unsigned', artifactPattern: /ios.*phone.*unsigned/i },
|
{ name: 'iOS Unsigned', platform: '🍎', device: '📱 Phone Unsigned', statusKey: 'iOS Unsigned', artifactPattern: /ios.*phone.*unsigned/i },
|
||||||
{ name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /ios.*tv.*ipa(?!.*unsigned)/i },
|
{ name: 'tvOS', platform: '🍎', device: '📺 TV', statusKey: 'tvOS', artifactPattern: /^(?!.*unsigned).*ios.*tv.*ipa/i },
|
||||||
{ name: 'tvOS Unsigned', platform: '🍎', device: '📺 TV Unsigned', statusKey: 'tvOS Unsigned', artifactPattern: /ios.*tv.*unsigned/i }
|
{ name: 'tvOS Unsigned', platform: '🍎', device: '📺 TV Unsigned', statusKey: 'tvOS Unsigned', artifactPattern: /ios.*tv.*unsigned/i }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -407,11 +456,9 @@ jobs:
|
|||||||
let durationInfo = '';
|
let durationInfo = '';
|
||||||
if (matchingStatus.started_at && matchingStatus.completed_at) {
|
if (matchingStatus.started_at && matchingStatus.completed_at) {
|
||||||
const durationMs = new Date(matchingStatus.completed_at) - new Date(matchingStatus.started_at);
|
const durationMs = new Date(matchingStatus.completed_at) - new Date(matchingStatus.started_at);
|
||||||
const durationMin = Math.floor(durationMs / 60000);
|
durationInfo = ` - ${fmtDuration(durationMs)}`;
|
||||||
const durationSec = Math.floor((durationMs % 60000) / 1000);
|
|
||||||
durationInfo = ` - ${durationMin}m ${durationSec}s`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadLink = `[📥 Download ${fileType}](${directLink}) ${sizeInfo}${durationInfo}`;
|
downloadLink = `[📥 Download ${fileType}](${directLink}) ${sizeInfo}${durationInfo}`;
|
||||||
} else if (matchingStatus.conclusion === 'failure') {
|
} else if (matchingStatus.conclusion === 'failure') {
|
||||||
status = `❌ [Failed](${matchingStatus.url})`;
|
status = `❌ [Failed](${matchingStatus.url})`;
|
||||||
@@ -421,10 +468,16 @@ jobs:
|
|||||||
downloadLink = '*Build cancelled*';
|
downloadLink = '*Build cancelled*';
|
||||||
} else if (matchingStatus.status === 'in_progress') {
|
} else if (matchingStatus.status === 'in_progress') {
|
||||||
status = `🔄 [Building...](${matchingStatus.url})`;
|
status = `🔄 [Building...](${matchingStatus.url})`;
|
||||||
downloadLink = '*Build in progress...*';
|
const ref = referenceDurations[target.statusKey];
|
||||||
|
downloadLink = ref
|
||||||
|
? `*Building… ~${fmtDuration(ref)} (avg on develop)*`
|
||||||
|
: '*Build in progress...*';
|
||||||
} else if (matchingStatus.status === 'queued') {
|
} else if (matchingStatus.status === 'queued') {
|
||||||
status = `⏳ [Queued](${matchingStatus.url})`;
|
status = `⏳ [Queued](${matchingStatus.url})`;
|
||||||
downloadLink = '*Waiting to start...*';
|
const ref = referenceDurations[target.statusKey];
|
||||||
|
downloadLink = ref
|
||||||
|
? `*Waiting to start… ~${fmtDuration(ref)} once running (avg on develop)*`
|
||||||
|
: '*Waiting to start...*';
|
||||||
} else if (matchingStatus.status === 'completed' && !matchingStatus.conclusion) {
|
} else if (matchingStatus.status === 'completed' && !matchingStatus.conclusion) {
|
||||||
// Workflow completed but conclusion not yet available (rare edge case)
|
// Workflow completed but conclusion not yet available (rare edge case)
|
||||||
status = `🔄 [Finishing...](${matchingStatus.url})`;
|
status = `🔄 [Finishing...](${matchingStatus.url})`;
|
||||||
@@ -445,26 +498,27 @@ jobs:
|
|||||||
|
|
||||||
commentBody += `\n`;
|
commentBody += `\n`;
|
||||||
|
|
||||||
// Show installation instructions if we have any artifacts
|
// Static rundown of the build optimisations + what each artifact
|
||||||
|
// installs on. Always shown (even mid-build) so testers know what
|
||||||
|
// to expect before downloads are ready.
|
||||||
|
commentBody += `<details>\n`;
|
||||||
|
commentBody += `<summary>📦 Build details & device compatibility</summary>\n\n`;
|
||||||
|
commentBody += `These CI builds are trimmed for size and speed. What that means for installing them:\n\n`;
|
||||||
|
commentBody += `| Artifact | Architectures | Installs on |\n`;
|
||||||
|
commentBody += `|---|---|---|\n`;
|
||||||
|
commentBody += `| 🤖 Android Phone APK | \`arm64-v8a\` | Every 64-bit Android phone (all since ~2017). **Not** an x86_64 emulator or a 32-bit device. |\n`;
|
||||||
|
commentBody += `| 📺 Android TV APK | \`arm64-v8a\` + \`armeabi-v7a\` | Modern boxes **and** older / cheap 32-bit Android TV sticks. No x86_64. |\n`;
|
||||||
|
commentBody += `| 🍎 iOS / tvOS IPA | \`arm64\` | iPhone / Apple TV (all current devices). |\n\n`;
|
||||||
|
commentBody += `**Why no x86_64?** That slice only runs on Android emulators / Chromebooks, never a real phone or TV box — dropping it shrinks the APK and speeds up the build. Local \`bun run android\` is unaffected (it still builds x86_64 from \`app.json\`).\n\n`;
|
||||||
|
commentBody += `**Runners:** Android on \`ubuntu-26.04\`; iOS / tvOS on Apple Silicon (\`macos-26\`). The size/speed win comes from the ABI trim above, not the runner.\n`;
|
||||||
|
commentBody += `</details>\n\n`;
|
||||||
|
|
||||||
|
// Installation instructions only matter once something is downloadable.
|
||||||
if (allArtifacts.length > 0) {
|
if (allArtifacts.length > 0) {
|
||||||
commentBody += `### 🔧 Installation Instructions\n\n`;
|
commentBody += `### 🔧 Installation Instructions\n\n`;
|
||||||
commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\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 += `- **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 += `> ⚠️ **Note**: Artifacts expire in 7 days from build date\n\n`;
|
||||||
|
|
||||||
// Collapsible rundown of the build optimisations + what each
|
|
||||||
// artifact actually installs on, so testers grab the right file.
|
|
||||||
commentBody += `<details>\n`;
|
|
||||||
commentBody += `<summary>📦 Build details & device compatibility</summary>\n\n`;
|
|
||||||
commentBody += `These CI builds are trimmed for size and speed. What that means for installing them:\n\n`;
|
|
||||||
commentBody += `| Artifact | Architectures | Installs on |\n`;
|
|
||||||
commentBody += `|---|---|---|\n`;
|
|
||||||
commentBody += `| 🤖 Android Phone APK | \`arm64-v8a\` | Every 64-bit Android phone (all since ~2017). **Not** an x86_64 emulator or a 32-bit device. |\n`;
|
|
||||||
commentBody += `| 📺 Android TV APK | \`arm64-v8a\` + \`armeabi-v7a\` | Modern boxes **and** older / cheap 32-bit Android TV sticks. No x86_64. |\n`;
|
|
||||||
commentBody += `| 🍎 iOS / tvOS IPA | \`arm64\` | iPhone / Apple TV (all current devices). |\n\n`;
|
|
||||||
commentBody += `**Why no x86_64?** That slice only runs on Android emulators / Chromebooks, never a real phone or TV box — dropping it shrinks the APK and speeds up the build. Local \`bun run android\` is unaffected (it still builds x86_64 from \`app.json\`).\n\n`;
|
|
||||||
commentBody += `**Runners:** Android on \`ubuntu-26.04\`; iOS / tvOS on Apple Silicon (\`macos-26\`). The size/speed win comes from the ABI trim above, not the runner.\n`;
|
|
||||||
commentBody += `</details>\n\n`;
|
|
||||||
} else {
|
} else {
|
||||||
commentBody += `⏳ **Builds are starting up...** This comment will update automatically as each build completes.\n\n`;
|
commentBody += `⏳ **Builds are starting up...** This comment will update automatically as each build completes.\n\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user