Repository navigation
vercel.deployment.success #723
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Preview links for changed pages | |
| # Comments on the PR with direct links to the docs pages it changed, since the | |
| # Vercel bot's own comment only links to the root of the preview deployment. | |
| # | |
| # Two triggers, one script: | |
| # - pull_request: reads the preview URL from the Vercel bot's comment, which | |
| # links the branch alias (…-git-<branch>-….vercel.app). The alias always | |
| # serves the branch's latest deployment, so the links stay right after each | |
| # push. Vercel posts that comment a few seconds after the PR opens, so the | |
| # script retries briefly. Pull requests from forks are skipped: their | |
| # GITHUB_TOKEN is read-only, so the comment cannot be posted. | |
| # - repository_dispatch: Vercel sends this when a deployment finishes. It covers | |
| # a PR opened before Vercel had commented, when the pull_request run gave up. | |
| # A deployment that finished before the PR was opened finds no PR and does | |
| # nothing; the pull_request run covers that case. | |
| # | |
| # GitHub only delivers repository_dispatch to the workflow file on the default | |
| # branch, so workflow_dispatch takes the same two payload fields as inputs for | |
| # re-running on a PR by hand: | |
| # gh workflow run preview-links.yml --ref <branch> -f url=<preview url> -f sha=<pr head sha> | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| repository_dispatch: | |
| types: [vercel.deployment.success] | |
| workflow_dispatch: | |
| inputs: | |
| url: | |
| description: Preview deployment URL (client_payload.url) | |
| required: true | |
| sha: | |
| description: Full commit SHA of the PR head (client_payload.git.sha) | |
| required: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| # A pull_request run and a dispatch run for the same commit queue up instead of | |
| # both creating the comment. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.head.sha || github.event.client_payload.git.sha || inputs.sha }} | |
| jobs: | |
| comment: | |
| if: >- | |
| github.event.client_payload.environment != 'production' | |
| && (github.event_name != 'pull_request' | |
| || github.event.pull_request.head.repo.full_name == github.repository) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/github-script@v7 | |
| env: | |
| PREVIEW_URL: ${{ github.event.client_payload.url || inputs.url }} | |
| COMMIT_SHA: ${{ github.event.client_payload.git.sha || inputs.sha }} | |
| with: | |
| script: | | |
| const {owner, repo} = context.repo; | |
| const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); | |
| async function listComments(pullNumber) { | |
| return github.paginate(github.rest.issues.listComments, { | |
| owner, repo, issue_number: pullNumber, per_page: 100 | |
| }); | |
| } | |
| // The Vercel bot's comment has a `[Preview](<branch alias>)` link per | |
| // project; this repository deploys one project. | |
| async function previewUrlFromVercelComment(pullNumber) { | |
| for (let attempt = 1; attempt <= 6; attempt++) { | |
| if (attempt > 1) await sleep(10_000); | |
| const vercelComment = (await listComments(pullNumber)).find(c => c.user?.login === 'vercel[bot]'); | |
| const match = vercelComment?.body?.match(/\[Preview\]\((https:\/\/[^)\s]+)\)/); | |
| if (match) return match[1]; | |
| core.info(`Vercel has not commented a preview URL on PR #${pullNumber} yet (attempt ${attempt}).`); | |
| } | |
| return null; | |
| } | |
| let pr, previewUrl; | |
| if (context.eventName === 'pull_request') { | |
| pr = context.payload.pull_request; | |
| previewUrl = await previewUrlFromVercelComment(pr.number); | |
| if (!previewUrl) { | |
| core.info(`Giving up; the vercel.deployment.success dispatch will comment when the deployment finishes.`); | |
| return; | |
| } | |
| } else { | |
| const {PREVIEW_URL, COMMIT_SHA} = process.env; | |
| if (!PREVIEW_URL || !/^[0-9a-f]{40}$/i.test(COMMIT_SHA ?? '')) { | |
| core.setFailed(`Need a preview URL and a full commit SHA; got url=${JSON.stringify(PREVIEW_URL)} sha=${JSON.stringify(COMMIT_SHA)}. ` | |
| + `Vercel sends these as client_payload.url and client_payload.git.sha; workflow_dispatch takes them as the url and sha inputs.`); | |
| return; | |
| } | |
| previewUrl = PREVIEW_URL; | |
| // The dispatch payload has no PR number; look it up from the commit. | |
| const {data: prs} = await github.rest.repos.listPullRequestsAssociatedWithCommit({ | |
| owner, repo, commit_sha: COMMIT_SHA | |
| }); | |
| pr = prs.find(p => p.state === 'open' && p.head.sha === COMMIT_SHA); | |
| if (!pr) { | |
| core.info(`No open PR with head ${COMMIT_SHA}; nothing to do.`); | |
| return; | |
| } | |
| } | |
| previewUrl = previewUrl.replace(/\/$/, ''); | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner, repo, pull_number: pr.number, per_page: 100 | |
| }); | |
| // Mirror contentlayer's flattenedPath: docs/<path>.mdx -> /<path>, | |
| // with a trailing /index dropped. Preview deployments have no basePath. | |
| const pages = files | |
| .filter(f => f.filename.startsWith('docs/') && f.filename.endsWith('.mdx') && f.status !== 'removed') | |
| .map(f => f.filename.slice('docs/'.length, -'.mdx'.length).replace(/^index$/, '').replace(/\/index$/, '')) | |
| .map(path => ({path, url: `${previewUrl}/${path}`})); | |
| if (pages.length === 0) { | |
| core.info(`PR #${pr.number} changes no docs pages; not commenting.`); | |
| return; | |
| } | |
| const marker = '<!-- preview-links-for-changed-pages -->'; | |
| const max = 40; | |
| const lines = pages.slice(0, max).map(p => `- [/${p.path}](${p.url})`); | |
| if (pages.length > max) lines.push(`- …and ${pages.length - max} more`); | |
| const body = [ | |
| marker, | |
| `Direct preview links to pages changed in this PR:`, | |
| ...lines | |
| ].join('\n'); | |
| // Update our previous comment instead of adding a new one per push. | |
| const existing = (await listComments(pr.number)).find(c => 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: pr.number, body}); | |
| } |