Skip to content

vercel.deployment.success #723

vercel.deployment.success

vercel.deployment.success #723

Workflow file for this run

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});
}