name: 📝 Artifact Comment on PR concurrency: 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] repository_dispatch: # Triggered by build workflows when they start/complete types: - build-started - build-completed - build-failed jobs: comment-artifacts: 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 repository_dispatch, pull_request, and manual dispatch events 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 (!payload || !payload.pr_number) { console.log('No PR information in repository_dispatch payload'); 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; } 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 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; } else { console.log('Unsupported event type:', context.eventName); return; } 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: targetCommitSha, per_page: 30 }); // 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`); // 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 = {}; // 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')); // 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 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: latestAndroidRun.id }); allArtifacts.push(...artifacts.artifacts); } catch (error) { 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); } } } // 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 += `### 📦 Build Artifacts\n\n`; commentBody += `| Platform | Device | Status | Download |\n`; commentBody += `|----------|--------|--------|---------|\n`; // 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' } ]; 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; } } // Find matching artifact const matchingArtifact = allArtifacts.find(artifact => target.pattern.test(artifact.name) ); let status = '⏳ Pending'; let downloadLink = '*Waiting for build...*'; 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 (matchingStatus.conclusion === 'failure') { status = `❌ [Failed](${matchingStatus.url})`; downloadLink = '*Build failed*'; } 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 { // Show any other status with timestamp for debugging status = `🔄 [${matchingStatus.status}](${matchingStatus.url})`; downloadLink = `*Status: ${matchingStatus.status}*`; } } commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`; } commentBody += `\n`; // Show installation instructions if we have any artifacts if (allArtifacts.length > 0) { commentBody += `### 🔧 Installation Instructions\n\n`; commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`; 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 += `⏳ **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})*`; 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('') ); 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}`); }