@@ -19,25 +19,22 @@ jobs:
1919 actions : read
2020 pull-requests : read
2121 outputs :
22- current : ${{ steps.resolve.outputs.current }}
23- number : ${{ steps.resolve.outputs.number }}
24- head_sha : ${{ steps.resolve.outputs.head_sha }}
22+ pull_requests : ${{ steps.resolve.outputs.pull_requests }}
2523 steps :
2624 # A forked workflow_run can omit pull request details, so the artifact
27- # must match the exact open pull request and commit.
28- - name : Resolve current pull request and artifact
25+ # must match the source repository, branch, and commit of each open PR .
26+ - name : Resolve current pull requests and artifact
2927 id : resolve
3028 uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
3129 with :
3230 script : |
3331 const run = context.payload.workflow_run;
3432 const expectedRepository = 'ruby/rdoc';
35- const expectedBase = 'master';
3633 const artifactName = 'pr-preview-site';
3734 const maximumArchiveBytes = 500 * 1024 * 1024;
35+ core.setOutput('pull_requests', '[]');
3836
3937 if (!['success', 'failure'].includes(run.conclusion)) {
40- core.setOutput('current', 'false');
4138 core.notice(`Skipped a preview build with conclusion: ${run.conclusion}.`);
4239 return;
4340 }
6663 repo: context.repo.repo,
6764 state: 'open',
6865 head: `${headOwner}:${headBranch}`,
69- base: expectedBase,
7066 per_page: 100,
7167 });
7268 candidateNumbers = new Set(
7773 }
7874
7975 if (candidateNumbers.size === 0) {
80- core.setOutput('current', 'false');
8176 core.notice('No open pull request uses this preview build.');
8277 return;
8378 }
@@ -93,25 +88,15 @@ jobs:
9388 }
9489
9590 const matches = candidates.filter(pull =>
91+ pull.state === 'open' &&
9692 pull.base.repo.full_name === expectedRepository &&
97- pull.base.ref === expectedBase &&
9893 pull.head.repo?.full_name === headRepository &&
99- pull.head.ref === headBranch
94+ pull.head.ref === headBranch &&
95+ pull.head.sha === run.head_sha
10096 );
10197
102- if (matches.length !== 1) {
103- core.setFailed(`Expected one pull request for this preview build, found ${matches.length}.`);
104- return;
105- }
106-
107- const pull = matches[0];
108- const current = pull.state === 'open' && pull.head.sha === run.head_sha;
109- core.setOutput('current', current.toString());
110- core.setOutput('number', pull.number.toString());
111- core.setOutput('head_sha', run.head_sha);
112-
113- if (!current) {
114- core.notice('The pull request changed or closed after this preview build.');
98+ if (matches.length === 0) {
99+ core.notice('No current open pull request uses this preview build.');
115100 return;
116101 }
117102
@@ -130,11 +115,26 @@ jobs:
130115 );
131116
132117 if (matchingArtifacts.length === 0 && run.conclusion === 'failure') {
133- core.setOutput('current', 'false');
134118 core.notice('The failed preview build did not publish an artifact.');
135119 return;
136120 }
137121
122+ // Title and body edits skip the build job but can still complete successfully.
123+ // Keep missing uploads an error when a build actually ran.
124+ if (matchingArtifacts.length === 0) {
125+ const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
126+ owner: context.repo.owner,
127+ repo: context.repo.repo,
128+ run_id: run.id,
129+ filter: 'latest',
130+ per_page: 100,
131+ });
132+ if (jobs.length > 0 && jobs.every(job => job.conclusion === 'skipped')) {
133+ core.notice('The preview build was skipped.');
134+ return;
135+ }
136+ }
137+
138138 if (matchingArtifacts.length !== 1) {
139139 core.setFailed(`Expected one ${artifactName} artifact, found ${matchingArtifacts.length}.`);
140140 return;
@@ -143,16 +143,30 @@ jobs:
143143 const artifact = matchingArtifacts[0];
144144 if (artifact.size_in_bytes <= 0 || artifact.size_in_bytes > maximumArchiveBytes) {
145145 core.setFailed(`The preview artifact archive has an invalid size: ${artifact.size_in_bytes} bytes.`);
146+ return;
146147 }
147148
149+ // The site comes from the head commit, not a merge with the base branch.
150+ // PRs with the same source repository, branch, and commit can share it.
151+ core.setOutput('pull_requests', JSON.stringify(matches.map(pull => ({
152+ number: pull.number,
153+ head_sha: run.head_sha,
154+ base_ref: pull.base.ref,
155+ }))));
156+
148157 deploy :
149- name : Deploy Preview
158+ name : Deploy Preview for PR ${{ matrix.pull_request.number }}
150159 needs : resolve
151- if : needs.resolve.outputs.current == 'true '
160+ if : needs.resolve.outputs.pull_requests != '[] '
152161 runs-on : ubuntu-latest
153162 timeout-minutes : 15
163+ # Each PR keeps its own preview and stale-data checks, even when the source is shared.
164+ strategy :
165+ fail-fast : false
166+ matrix :
167+ pull_request : ${{ fromJSON(needs.resolve.outputs.pull_requests) }}
154168 concurrency :
155- group : pr-preview-deploy-${{ needs.resolve.outputs .number }}
169+ group : pr-preview-deploy-${{ matrix.pull_request .number }}
156170 cancel-in-progress : true
157171 permissions :
158172 actions : read
@@ -254,18 +268,22 @@ jobs:
254268 core.info(`Accepted ${fileCount} static files (${totalBytes} bytes).`);
255269
256270 # The pull request can change after artifact selection. A second head
257- # comparison blocks deployment of a stale commit .
258- - name : Confirm pull request head
271+ # and base comparison blocks deployment with stale pull request data .
272+ - name : Confirm pull request head and base
259273 id : current
260274 uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
261275 env :
262- PR_NUMBER : ${{ needs.resolve.outputs.number }}
263- EXPECTED_SHA : ${{ needs.resolve.outputs.head_sha }}
276+ PR_NUMBER : ${{ matrix.pull_request.number }}
277+ EXPECTED_SHA : ${{ matrix.pull_request.head_sha }}
278+ EXPECTED_BASE_REF : ${{ matrix.pull_request.base_ref }}
264279 with :
265280 script : |
266281 const number = process.env.PR_NUMBER;
267282 const expectedSha = process.env.EXPECTED_SHA;
268- if (!/^[1-9][0-9]*$/.test(number) || !/^[0-9a-f]{40}$/.test(expectedSha)) {
283+ const expectedBaseRef = process.env.EXPECTED_BASE_REF;
284+ if (!/^[1-9][0-9]*$/.test(number) ||
285+ !/^[0-9a-f]{40}$/.test(expectedSha) ||
286+ !expectedBaseRef) {
269287 core.setFailed('The resolved pull request metadata is invalid.');
270288 return;
271289 }
@@ -278,7 +296,7 @@ jobs:
278296 const current =
279297 pull.state === 'open' &&
280298 pull.base.repo.full_name === 'ruby/rdoc' &&
281- pull.base.ref === 'master' &&
299+ pull.base.ref === expectedBaseRef &&
282300 pull.head.sha === expectedSha;
283301
284302 core.setOutput('current', current.toString());
@@ -305,29 +323,33 @@ jobs:
305323 command : >-
306324 pages deploy "${{ runner.temp }}/pr-preview-site"
307325 --project-name=rdoc
308- --branch="${{ needs.resolve.outputs .number }}-preview"
309- --commit-hash="${{ needs.resolve.outputs .head_sha }}"
326+ --branch="${{ matrix.pull_request .number }}-preview"
327+ --commit-hash="${{ matrix.pull_request .head_sha }}"
310328
311- # The workflow reuses one marked comment to avoid notification spam and
312- # show the exact commit for the preview .
329+ # The workflow rechecks the pull request after deployment, then reuses
330+ # one marked comment to avoid stale links and notification spam .
313331 - name : Update preview comment
314332 if : steps.current.outputs.current == 'true'
315333 uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
316334 env :
317- PR_NUMBER : ${{ needs.resolve.outputs.number }}
318- EXPECTED_SHA : ${{ needs.resolve.outputs.head_sha }}
335+ PR_NUMBER : ${{ matrix.pull_request.number }}
336+ EXPECTED_SHA : ${{ matrix.pull_request.head_sha }}
337+ EXPECTED_BASE_REF : ${{ matrix.pull_request.base_ref }}
319338 PREVIEW_ALIAS_URL : ${{ steps.deploy.outputs.pages-deployment-alias-url }}
320339 PREVIEW_DEPLOYMENT_URL : ${{ steps.deploy.outputs.deployment-url }}
321340 with :
322341 script : |
323342 const marker = '<!-- rdoc-pr-preview -->';
324343 const number = process.env.PR_NUMBER;
325344 const expectedSha = process.env.EXPECTED_SHA;
345+ const expectedBaseRef = process.env.EXPECTED_BASE_REF;
326346 const previewUrl = (
327347 process.env.PREVIEW_ALIAS_URL || process.env.PREVIEW_DEPLOYMENT_URL || ''
328348 ).trim();
329349
330- if (!/^[1-9][0-9]*$/.test(number) || !/^[0-9a-f]{40}$/.test(expectedSha)) {
350+ if (!/^[1-9][0-9]*$/.test(number) ||
351+ !/^[0-9a-f]{40}$/.test(expectedSha) ||
352+ !expectedBaseRef) {
331353 core.setFailed('The preview comment metadata is invalid.');
332354 return;
333355 }
@@ -353,7 +375,7 @@ jobs:
353375 const current =
354376 pull.state === 'open' &&
355377 pull.base.repo.full_name === 'ruby/rdoc' &&
356- pull.base.ref === 'master' &&
378+ pull.base.ref === expectedBaseRef &&
357379 pull.head.sha === expectedSha;
358380 if (!current) {
359381 core.notice('Skipped the preview comment because the pull request changed.');
0 commit comments