Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 64 additions & 42 deletions .github/workflows/cloudflare-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,22 @@ jobs:
actions: read
pull-requests: read
outputs:
current: ${{ steps.resolve.outputs.current }}
number: ${{ steps.resolve.outputs.number }}
head_sha: ${{ steps.resolve.outputs.head_sha }}
pull_requests: ${{ steps.resolve.outputs.pull_requests }}
steps:
# A forked workflow_run can omit pull request details, so the artifact
# must match the exact open pull request and commit.
- name: Resolve current pull request and artifact
# must match the source repository, branch, and commit of each open PR.
- name: Resolve current pull requests and artifact
id: resolve
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const run = context.payload.workflow_run;
const expectedRepository = 'ruby/rdoc';
const expectedBase = 'master';
const artifactName = 'pr-preview-site';
const maximumArchiveBytes = 500 * 1024 * 1024;
core.setOutput('pull_requests', '[]');

if (!['success', 'failure'].includes(run.conclusion)) {
core.setOutput('current', 'false');
core.notice(`Skipped a preview build with conclusion: ${run.conclusion}.`);
return;
}
Expand Down Expand Up @@ -66,7 +63,6 @@ jobs:
repo: context.repo.repo,
state: 'open',
head: `${headOwner}:${headBranch}`,
base: expectedBase,
per_page: 100,
});
candidateNumbers = new Set(
Expand All @@ -77,7 +73,6 @@ jobs:
}

if (candidateNumbers.size === 0) {
core.setOutput('current', 'false');
core.notice('No open pull request uses this preview build.');
return;
}
Expand All @@ -93,25 +88,15 @@ jobs:
}

const matches = candidates.filter(pull =>
pull.state === 'open' &&
pull.base.repo.full_name === expectedRepository &&
pull.base.ref === expectedBase &&
pull.head.repo?.full_name === headRepository &&
pull.head.ref === headBranch
pull.head.ref === headBranch &&
pull.head.sha === run.head_sha
);

if (matches.length !== 1) {
core.setFailed(`Expected one pull request for this preview build, found ${matches.length}.`);
return;
}

const pull = matches[0];
const current = pull.state === 'open' && pull.head.sha === run.head_sha;
core.setOutput('current', current.toString());
core.setOutput('number', pull.number.toString());
core.setOutput('head_sha', run.head_sha);

if (!current) {
core.notice('The pull request changed or closed after this preview build.');
if (matches.length === 0) {
core.notice('No current open pull request uses this preview build.');
return;
}

Expand All @@ -130,11 +115,26 @@ jobs:
);

if (matchingArtifacts.length === 0 && run.conclusion === 'failure') {
core.setOutput('current', 'false');
core.notice('The failed preview build did not publish an artifact.');
return;
}

// Title and body edits skip the build job but can still complete successfully.
// Keep missing uploads an error when a build actually ran.
if (matchingArtifacts.length === 0) {
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: run.id,
filter: 'latest',
per_page: 100,
});
if (jobs.length > 0 && jobs.every(job => job.conclusion === 'skipped')) {
core.notice('The preview build was skipped.');
return;
}
}

if (matchingArtifacts.length !== 1) {
core.setFailed(`Expected one ${artifactName} artifact, found ${matchingArtifacts.length}.`);
return;
Expand All @@ -143,16 +143,30 @@ jobs:
const artifact = matchingArtifacts[0];
if (artifact.size_in_bytes <= 0 || artifact.size_in_bytes > maximumArchiveBytes) {
core.setFailed(`The preview artifact archive has an invalid size: ${artifact.size_in_bytes} bytes.`);
return;
}

// The site comes from the head commit, not a merge with the base branch.
// PRs with the same source repository, branch, and commit can share it.
core.setOutput('pull_requests', JSON.stringify(matches.map(pull => ({
number: pull.number,
head_sha: run.head_sha,
base_ref: pull.base.ref,
}))));

deploy:
name: Deploy Preview
name: Deploy Preview for PR ${{ matrix.pull_request.number }}
needs: resolve
if: needs.resolve.outputs.current == 'true'
if: needs.resolve.outputs.pull_requests != '[]'
runs-on: ubuntu-latest
timeout-minutes: 15
# Each PR keeps its own preview and stale-data checks, even when the source is shared.
strategy:
fail-fast: false
matrix:
pull_request: ${{ fromJSON(needs.resolve.outputs.pull_requests) }}
concurrency:
group: pr-preview-deploy-${{ needs.resolve.outputs.number }}
group: pr-preview-deploy-${{ matrix.pull_request.number }}
cancel-in-progress: true
permissions:
actions: read
Expand Down Expand Up @@ -254,18 +268,22 @@ jobs:
core.info(`Accepted ${fileCount} static files (${totalBytes} bytes).`);

# The pull request can change after artifact selection. A second head
# comparison blocks deployment of a stale commit.
- name: Confirm pull request head
# and base comparison blocks deployment with stale pull request data.
- name: Confirm pull request head and base
id: current
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ needs.resolve.outputs.number }}
EXPECTED_SHA: ${{ needs.resolve.outputs.head_sha }}
PR_NUMBER: ${{ matrix.pull_request.number }}
EXPECTED_SHA: ${{ matrix.pull_request.head_sha }}
EXPECTED_BASE_REF: ${{ matrix.pull_request.base_ref }}
with:
script: |
const number = process.env.PR_NUMBER;
const expectedSha = process.env.EXPECTED_SHA;
if (!/^[1-9][0-9]*$/.test(number) || !/^[0-9a-f]{40}$/.test(expectedSha)) {
const expectedBaseRef = process.env.EXPECTED_BASE_REF;
if (!/^[1-9][0-9]*$/.test(number) ||
!/^[0-9a-f]{40}$/.test(expectedSha) ||
!expectedBaseRef) {
core.setFailed('The resolved pull request metadata is invalid.');
return;
}
Expand All @@ -278,7 +296,7 @@ jobs:
const current =
pull.state === 'open' &&
pull.base.repo.full_name === 'ruby/rdoc' &&
pull.base.ref === 'master' &&
pull.base.ref === expectedBaseRef &&
pull.head.sha === expectedSha;

core.setOutput('current', current.toString());
Expand All @@ -305,29 +323,33 @@ jobs:
command: >-
pages deploy "${{ runner.temp }}/pr-preview-site"
--project-name=rdoc
--branch="${{ needs.resolve.outputs.number }}-preview"
--commit-hash="${{ needs.resolve.outputs.head_sha }}"
--branch="${{ matrix.pull_request.number }}-preview"
--commit-hash="${{ matrix.pull_request.head_sha }}"

# The workflow reuses one marked comment to avoid notification spam and
# show the exact commit for the preview.
# The workflow rechecks the pull request after deployment, then reuses
# one marked comment to avoid stale links and notification spam.
- name: Update preview comment
if: steps.current.outputs.current == 'true'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ needs.resolve.outputs.number }}
EXPECTED_SHA: ${{ needs.resolve.outputs.head_sha }}
PR_NUMBER: ${{ matrix.pull_request.number }}
EXPECTED_SHA: ${{ matrix.pull_request.head_sha }}
EXPECTED_BASE_REF: ${{ matrix.pull_request.base_ref }}
PREVIEW_ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
PREVIEW_DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
with:
script: |
const marker = '<!-- rdoc-pr-preview -->';
const number = process.env.PR_NUMBER;
const expectedSha = process.env.EXPECTED_SHA;
const expectedBaseRef = process.env.EXPECTED_BASE_REF;
const previewUrl = (
process.env.PREVIEW_ALIAS_URL || process.env.PREVIEW_DEPLOYMENT_URL || ''
).trim();

if (!/^[1-9][0-9]*$/.test(number) || !/^[0-9a-f]{40}$/.test(expectedSha)) {
if (!/^[1-9][0-9]*$/.test(number) ||
!/^[0-9a-f]{40}$/.test(expectedSha) ||
!expectedBaseRef) {
core.setFailed('The preview comment metadata is invalid.');
return;
}
Expand All @@ -353,7 +375,7 @@ jobs:
const current =
pull.state === 'open' &&
pull.base.repo.full_name === 'ruby/rdoc' &&
pull.base.ref === 'master' &&
pull.base.ref === expectedBaseRef &&
pull.head.sha === expectedSha;
if (!current) {
core.notice('Skipped the preview comment because the pull request changed.');
Expand Down
15 changes: 9 additions & 6 deletions .github/workflows/pr-preview-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@ name: Build PR Preview

on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, edited]

permissions:
contents: read

concurrency:
group: pr-preview-build-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
build:
name: Build Preview
if: github.repository == 'ruby/rdoc'
# Base retargets need a preview, but title and body edits do not change the site.
if: >-
github.repository == 'ruby/rdoc' &&
(github.event.action != 'edited' || github.event.changes.base)
runs-on: ubuntu-latest
timeout-minutes: 20
# A skipped edit must not cancel an active build for the same PR.
concurrency:
group: pr-preview-build-${{ github.event.pull_request.number }}
cancel-in-progress: true
steps:
# This job executes untrusted pull request code. It must not receive secrets
# or a token with write access.
Expand Down