Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/vercel-build-log.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +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 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=<team>` and `projectId` in the body.
Expand Down
111 changes: 101 additions & 10 deletions dev/vercel-build-log.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
* 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,
* 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
Expand All @@ -48,6 +51,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;
Expand All @@ -57,6 +64,14 @@ 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 = '<!-- vercel-build-log -->';
const MARKER_PATTERN = /^<!-- vercel-build-log(?: artifact=\d+)? -->/;
// The comment .github/workflows/preview-links.yml leaves on a PR
const PREVIEW_LINKS_MARKER = '<!-- preview-links-for-changed-pages -->';
// 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));
}

// ---------------------------------------------------------------- GitHub

Expand Down Expand Up @@ -313,9 +328,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);
}
}

Expand All @@ -339,18 +352,85 @@ function pullRequestLinks(pulls) {
.join(', ');
}

// The pages the PR changed, from the preview-links workflow's PR comment, as
// Slack links: its `- [/path](preview url)` lines become `• <url|/path>`,
// 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`
);
const comment = comments.find(comment =>
comment.body.includes(PREVIEW_LINKS_MARKER)
);
if (comment) {
return changedPageLines(pull, comment.body);
}
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);
}
}

// 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) {
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
// 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 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}, 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)}`
`Build log for PR ${pullRequestLinks(pulls)}`,
...changedPages
]
.filter(Boolean)
.join('\n');
Expand Down Expand Up @@ -420,7 +500,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 changedPages = await changedPageLinks(pulls);
const permalink = await uploadLogToThread(
post,
pulls,
buildLog,
changedPages
);
return {channel, permalink};
}

Expand Down
Loading