# Copyright (C) 2026 SharpEmu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later # Posts (and keeps updated) a single PR comment linking the per-platform build # artifacts once "Build and Release" finishes. # # This is a workflow_run workflow on purpose: PRs from forks run "Build and # Release" with a read-only GITHUB_TOKEN and cannot comment. workflow_run runs # afterwards in the base-repo context with a read-write token and does not check # out untrusted fork code, so it can comment safely. Because of that, GitHub only # triggers it from the copy on the default branch — it does nothing until merged # to main. name: PR Build Links on: workflow_run: workflows: ["Build and Release"] types: - completed permissions: contents: read actions: read pull-requests: write jobs: comment: name: Post artifact links runs-on: ubuntu-latest # Only for successful PR builds — artifacts exist only when the build passed. if: >- github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - name: Post or update the artifact-links comment uses: actions/github-script@v7 with: script: | const run = context.payload.workflow_run; const { owner, repo } = context.repo; // Resolve the PR. Same-repo PRs are in workflow_run.pull_requests, but // fork PRs leave it empty AND their head commit is not on a base-repo // branch, so listPullRequestsAssociatedWithCommit misses them too. Match // the open PR by its head "owner:branch" instead, which works for forks. let prNumber; if (run.pull_requests && run.pull_requests.length > 0) { prNumber = run.pull_requests[0].number; } else { let candidates = []; const headOwner = run.head_repository && run.head_repository.owner ? run.head_repository.owner.login : null; if (headOwner && run.head_branch) { const byHead = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${headOwner}:${run.head_branch}`, per_page: 10, }); candidates = byHead.data; } if (candidates.length === 0) { const byCommit = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner, repo, commit_sha: run.head_sha, }); candidates = byCommit.data.filter(pr => pr.state === 'open'); } if (candidates.length === 0) { core.info('No open PR for this build; nothing to comment.'); return; } prNumber = candidates[0].number; } // Collect the per-platform artifacts the build produced. const artifacts = await github.paginate( github.rest.actions.listWorkflowRunArtifacts, { owner, repo, run_id: run.id, per_page: 100 }, ); const platforms = [ { key: 'win-x64', label: 'Windows (win-x64)' }, { key: 'linux-x64', label: 'Linux (linux-x64)' }, { key: 'osx-x64', label: 'macOS (osx-x64)' }, ]; const rows = []; for (const platform of platforms) { const artifact = artifacts.find(a => a.name.includes(platform.key)); if (!artifact) { continue; } const url = `https://github.com/${owner}/${repo}/actions/runs/${run.id}/artifacts/${artifact.id}`; rows.push(`| ${platform.label} | [\`${artifact.name}\`](${url}) |`); } if (rows.length === 0) { core.info('No platform artifacts on this run; nothing to comment.'); return; } const marker = ''; const body = [ marker, `### 📦 Build artifacts — \`${run.head_sha.substring(0, 7)}\``, '', '| Platform | Download |', '| --- | --- |', ...rows, '', `From [build run #${run.run_number}](${run.html_url}). ` + 'Downloads require a GitHub login and expire after 90 days.', ].join('\n'); // Upsert one comment so repeated builds refresh it in place. const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber, per_page: 100, }); const existing = comments.find(c => c.body && c.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); } else { await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body }); }