From 759cbfd74437e1500458c9aab15ec9e5da890de9 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:25:01 -0600 Subject: [PATCH 1/2] Add the PR's preview page links to the Vercel build log Slack reply The preview-links workflow comments on each PR with direct links to the docs pages it changed on the preview deployment. Copy those links, as Slack links, into the build log reply on the Vercel app's Slack post, so readers get to the changed pages without opening the PR. Only for a preview deployment that built: a failed build has no preview, and a merged PR's preview is behind production. The comment is looked for up to a minute, since the preview-links workflow runs on the same event. --- .github/workflows/vercel-build-log.yml | 4 +- AGENTS.md | 2 + dev/vercel-build-log.mjs | 96 +++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/.github/workflows/vercel-build-log.yml b/.github/workflows/vercel-build-log.yml index 9dcdcfdfd..90e77ebb2 100644 --- a/.github/workflows/vercel-build-log.yml +++ b/.github/workflows/vercel-build-log.yml @@ -5,7 +5,9 @@ name: Vercel build log # each deployment (SLACK_BOT_TOKEN secret and SLACK_CHANNEL_ID variable; see # dev/vercel-build-log-slack-app-manifest.json), with a link to the PR: the # open PR at the commit for a preview deployment, or the merged PR that -# introduced it for a production one. When a PR's build fails, the PR also +# introduced it for a production one. A preview that built also gets the +# preview-links workflow's links to the pages the PR changed, copied from +# its PR comment. When a PR's build fails, the PR also # gets a comment linking to the log; when a later revision builds, the # comment is updated to say so. The log never goes on the PR itself, so # anything sensitive a build prints stays in Slack instead of a public diff --git a/AGENTS.md b/AGENTS.md index b4021bca9..4c9904a6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,6 +135,8 @@ next run. Slack app's "failed to deploy" post in the `SLACK_CHANNEL_ID` channel and comments a link to it on the PR (see `dev/vercel-build-log.mjs`). The log itself never goes on the PR, since the repository is public. + A preview that built also gets the `preview-links.yml` comment's page + links in the Slack reply. - It reads Vercel with the `VERCEL_TOKEN` repo secret, a token scoped to the `sourcegraph-docs` project that expires 2026-12-10; mint a new one with `POST /v3/user/tokens?teamId=` and `projectId` in the body. diff --git a/dev/vercel-build-log.mjs b/dev/vercel-build-log.mjs index 9813ca0f3..976a6e195 100644 --- a/dev/vercel-build-log.mjs +++ b/dev/vercel-build-log.mjs @@ -24,7 +24,9 @@ * such as on an invalid vercel.json, has an error but no log lines), and * redacted. * 3. The log is uploaded into the thread of the Vercel Slack app's post for - * the deployment in SLACK_CHANNEL_ID, with a comment linking the PR. The + * the deployment in SLACK_CHANNEL_ID, with a comment linking the PR and, + * for a preview deployment that built, the direct links to the docs pages + * the PR changed, copied from the preview-links workflow's PR comment. The * post is looked for a week back (so a re-run by hand still finds it) and * for up to 5 minutes (the app can post after this runs). Skipped when * SLACK_BOT_TOKEN or SLACK_CHANNEL_ID is unset; a Slack error is a @@ -48,6 +50,10 @@ const DRY_RUN = process.argv.includes('--dry-run'); const SLACK_HISTORY_DAYS = 7; const SLACK_WAIT_MINUTES = 5; const SLACK_POLL_SECONDS = 15; +// The preview-links workflow comments on the PR when the same deployment +// event arrives, so it can still be running when this looks for its comment +const PREVIEW_LINKS_WAIT_MINUTES = 1; +const PREVIEW_LINKS_POLL_SECONDS = 10; const API_URL = process.env.GITHUB_API_URL ?? 'https://api.github.com'; const REPOSITORY = process.env.GITHUB_REPOSITORY; @@ -57,6 +63,12 @@ const {DEPLOYMENT_ID, DEPLOYMENT_STATE, COMMIT_SHA, SLACK_CHANNEL_ID} = // Comments from before the log moved to Slack carry an artifact ID here const MARKER = ''; const MARKER_PATTERN = /^/; +// The comment .github/workflows/preview-links.yml leaves on a PR +const PREVIEW_LINKS_MARKER = ''; + +function sleep(seconds) { + return new Promise(resolve => setTimeout(resolve, seconds * 1000)); +} // ---------------------------------------------------------------- GitHub @@ -313,9 +325,7 @@ async function findVercelPost() { console.log( `No Vercel post for ${DEPLOYMENT_ID} yet; checking again in ${SLACK_POLL_SECONDS}s` ); - await new Promise(resolve => - setTimeout(resolve, SLACK_POLL_SECONDS * 1000) - ); + await sleep(SLACK_POLL_SECONDS); } } @@ -339,18 +349,73 @@ function pullRequestLinks(pulls) { .join(', '); } +// The preview-links workflow's PR comment, which links each docs page the PR +// changed on the preview, as Slack text: its `- [/path](url)` lines become +// `• `, and the marker goes. Vercel's own post only links the +// preview's root. That workflow is started by the same deployment event as +// this one, so give it a minute to comment before going without. +async function previewLinkLines(pull) { + const deadline = Date.now() + PREVIEW_LINKS_WAIT_MINUTES * 60_000; + for (;;) { + const comments = await githubList( + `/repos/${REPOSITORY}/issues/${pull.number}/comments` + ); + const comment = comments.find(comment => + comment.body.includes(PREVIEW_LINKS_MARKER) + ); + if (comment) { + return comment.body + .split('\n') + .filter(line => line !== PREVIEW_LINKS_MARKER) + .map(line => + line + .replace(/^- /, '• ') + .replace( + /\[([^\]]+)\]\((https:\/\/[^)\s]+)\)/, + (link, label, url) => + `<${url}|${escapeSlackText(label)}>` + ) + ); + } + if (Date.now() >= deadline) { + console.log( + `PR #${pull.number} has no preview links comment; not listing its pages` + ); + return []; + } + console.log( + `PR #${pull.number} has no preview links comment yet; checking again in ${PREVIEW_LINKS_POLL_SECONDS}s` + ); + await sleep(PREVIEW_LINKS_POLL_SECONDS); + } +} + +// Preview links for the open PRs of a deployment that built: a failed build +// has no preview, and a merged PR's preview is behind production +async function previewLinks(pulls) { + if (DEPLOYMENT_STATE !== 'success') { + return []; + } + const lines = []; + for (const pull of pulls.filter(pull => pull.state === 'open')) { + lines.push(...(await previewLinkLines(pull))); + } + return lines; +} + // Slack takes files in three steps: ask for an upload URL, POST the bytes to // it, then say which channel and thread to share the file in. The PR link -// rides on the file's comment, which is the one message the bot can post -// without chat:write. Returns the permalink of the reply carrying the file, -// for the PR comment. -async function uploadLogToThread(post, pulls, {log, error}) { +// and preview links ride on the file's comment, which is the one message the +// bot can post without chat:write. Returns the permalink of the reply +// carrying the file, for the PR comment. +async function uploadLogToThread(post, pulls, {log, error}, previewLines) { const shortSha = COMMIT_SHA.slice(0, 7); // .txt, so Slack shows it inline instead of offering a download const filename = `vercel-build-${shortSha}.txt`; const initialComment = [ error && `> ${error}`, - `Build log for PR ${pullRequestLinks(pulls)}` + `Build log for PR ${pullRequestLinks(pulls)}`, + ...previewLines ] .filter(Boolean) .join('\n'); @@ -420,7 +485,18 @@ async function attachLogInSlack(pulls, buildLog) { ); } const post = await findVercelPost(); - const permalink = post && (await uploadLogToThread(post, pulls, buildLog)); + if (!post) { + return {channel}; + } + // After the post is found, so the preview-links workflow has had that + // long to comment too + const previewLines = await previewLinks(pulls); + const permalink = await uploadLogToThread( + post, + pulls, + buildLog, + previewLines + ); return {channel, permalink}; } From a01f2ef8cab713f8863618988e0a757996720b63 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:43:06 -0600 Subject: [PATCH 2/2] Link a merged PR's changed pages in production The Slack reply on a production deployment's post now lists the pages the merged PR changed, at https://sourcegraph.com/docs/, using the same preview-links PR comment. A merged PR's comment is there or never coming, so it is not waited for. --- .github/workflows/vercel-build-log.yml | 6 +- AGENTS.md | 5 +- dev/vercel-build-log.mjs | 77 +++++++++++++++----------- 3 files changed, 52 insertions(+), 36 deletions(-) diff --git a/.github/workflows/vercel-build-log.yml b/.github/workflows/vercel-build-log.yml index 90e77ebb2..49d66b2a4 100644 --- a/.github/workflows/vercel-build-log.yml +++ b/.github/workflows/vercel-build-log.yml @@ -5,9 +5,9 @@ name: Vercel build log # each deployment (SLACK_BOT_TOKEN secret and SLACK_CHANNEL_ID variable; see # dev/vercel-build-log-slack-app-manifest.json), with a link to the PR: the # open PR at the commit for a preview deployment, or the merged PR that -# introduced it for a production one. A preview that built also gets the -# preview-links workflow's links to the pages the PR changed, copied from -# its PR comment. When a PR's build fails, the PR also +# introduced it for a production one. A build that passed also lists the +# pages the PR changed, from the preview-links workflow's PR comment, on the +# preview or in production. When a PR's build fails, the PR also # gets a comment linking to the log; when a later revision builds, the # comment is updated to say so. The log never goes on the PR itself, so # anything sensitive a build prints stays in Slack instead of a public diff --git a/AGENTS.md b/AGENTS.md index 4c9904a6c..5ff1a5567 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,8 +135,9 @@ next run. Slack app's "failed to deploy" post in the `SLACK_CHANNEL_ID` channel and comments a link to it on the PR (see `dev/vercel-build-log.mjs`). The log itself never goes on the PR, since the repository is public. - A preview that built also gets the `preview-links.yml` comment's page - links in the Slack reply. + A build that passed also lists the pages the PR changed in the Slack + reply, from the `preview-links.yml` comment: on the preview for an open + PR, in production for a merged one. - It reads Vercel with the `VERCEL_TOKEN` repo secret, a token scoped to the `sourcegraph-docs` project that expires 2026-12-10; mint a new one with `POST /v3/user/tokens?teamId=` and `projectId` in the body. diff --git a/dev/vercel-build-log.mjs b/dev/vercel-build-log.mjs index 976a6e195..5dd39b4e0 100644 --- a/dev/vercel-build-log.mjs +++ b/dev/vercel-build-log.mjs @@ -25,8 +25,9 @@ * redacted. * 3. The log is uploaded into the thread of the Vercel Slack app's post for * the deployment in SLACK_CHANNEL_ID, with a comment linking the PR and, - * for a preview deployment that built, the direct links to the docs pages - * the PR changed, copied from the preview-links workflow's PR comment. The + * when the build passed, the docs pages the PR changed, taken from the + * preview-links workflow's PR comment: on the preview for an open PR, in + * production for a merged one. The * post is looked for a week back (so a re-run by hand still finds it) and * for up to 5 minutes (the app can post after this runs). Skipped when * SLACK_BOT_TOKEN or SLACK_CHANNEL_ID is unset; a Slack error is a @@ -65,6 +66,8 @@ const MARKER = ''; const MARKER_PATTERN = /^/; // The comment .github/workflows/preview-links.yml leaves on a PR const PREVIEW_LINKS_MARKER = ''; +// Where a merged PR's pages are (basePath in next.config.js) +const PRODUCTION_SITE = 'https://sourcegraph.com/docs'; function sleep(seconds) { return new Promise(resolve => setTimeout(resolve, seconds * 1000)); @@ -349,13 +352,36 @@ function pullRequestLinks(pulls) { .join(', '); } -// The preview-links workflow's PR comment, which links each docs page the PR -// changed on the preview, as Slack text: its `- [/path](url)` lines become -// `• `, and the marker goes. Vercel's own post only links the -// preview's root. That workflow is started by the same deployment event as -// this one, so give it a minute to comment before going without. -async function previewLinkLines(pull) { - const deadline = Date.now() + PREVIEW_LINKS_WAIT_MINUTES * 60_000; +// The pages the PR changed, from the preview-links workflow's PR comment, as +// Slack links: its `- [/path](preview url)` lines become `• `, +// pointing at the preview for an open PR and at production for a merged one, +// where the pages now are. The comment's `- …and N more` line is kept. +function changedPageLines(pull, commentBody) { + const production = Boolean(pull.merged_at); + const lines = []; + for (const line of commentBody.split('\n')) { + const link = line.match(/^- \[([^\]]+)\]\((https:\/\/[^)\s]+)\)$/); + if (link) { + const [, path, previewUrl] = link; + const url = production ? `${PRODUCTION_SITE}${path}` : previewUrl; + lines.push(`• <${url}|${escapeSlackText(path)}>`); + } else if (line.startsWith('- ')) { + lines.push(line.replace('- ', '• ')); + } + } + if (lines.length === 0) { + return []; + } + const where = production ? 'in production' : 'on the preview'; + return [`Pages changed in this PR, ${where}:`, ...lines]; +} + +// The preview-links workflow is started by the same deployment event as this +// one, so an open PR gets a minute to be commented on. A merged PR's comment +// is either there already or never coming. +async function findChangedPageLines(pull) { + const wait = pull.state === 'open' ? PREVIEW_LINKS_WAIT_MINUTES : 0; + const deadline = Date.now() + wait * 60_000; for (;;) { const comments = await githubList( `/repos/${REPOSITORY}/issues/${pull.number}/comments` @@ -364,18 +390,7 @@ async function previewLinkLines(pull) { comment.body.includes(PREVIEW_LINKS_MARKER) ); if (comment) { - return comment.body - .split('\n') - .filter(line => line !== PREVIEW_LINKS_MARKER) - .map(line => - line - .replace(/^- /, '• ') - .replace( - /\[([^\]]+)\]\((https:\/\/[^)\s]+)\)/, - (link, label, url) => - `<${url}|${escapeSlackText(label)}>` - ) - ); + return changedPageLines(pull, comment.body); } if (Date.now() >= deadline) { console.log( @@ -390,32 +405,32 @@ async function previewLinkLines(pull) { } } -// Preview links for the open PRs of a deployment that built: a failed build -// has no preview, and a merged PR's preview is behind production -async function previewLinks(pulls) { +// Links to the changed pages for a deployment that built; a failed build +// changed nothing anyone can open +async function changedPageLinks(pulls) { if (DEPLOYMENT_STATE !== 'success') { return []; } const lines = []; - for (const pull of pulls.filter(pull => pull.state === 'open')) { - lines.push(...(await previewLinkLines(pull))); + for (const pull of pulls) { + lines.push(...(await findChangedPageLines(pull))); } return lines; } // Slack takes files in three steps: ask for an upload URL, POST the bytes to // it, then say which channel and thread to share the file in. The PR link -// and preview links ride on the file's comment, which is the one message the +// and page links ride on the file's comment, which is the one message the // bot can post without chat:write. Returns the permalink of the reply // carrying the file, for the PR comment. -async function uploadLogToThread(post, pulls, {log, error}, previewLines) { +async function uploadLogToThread(post, pulls, {log, error}, changedPages) { const shortSha = COMMIT_SHA.slice(0, 7); // .txt, so Slack shows it inline instead of offering a download const filename = `vercel-build-${shortSha}.txt`; const initialComment = [ error && `> ${error}`, `Build log for PR ${pullRequestLinks(pulls)}`, - ...previewLines + ...changedPages ] .filter(Boolean) .join('\n'); @@ -490,12 +505,12 @@ async function attachLogInSlack(pulls, buildLog) { } // After the post is found, so the preview-links workflow has had that // long to comment too - const previewLines = await previewLinks(pulls); + const changedPages = await changedPageLinks(pulls); const permalink = await uploadLogToThread( post, pulls, buildLog, - previewLines + changedPages ); return {channel, permalink}; }