From ca02e025e021224772a06aad95114c2a33b667cc Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:05:07 -0600 Subject: [PATCH 01/10] ci: Comment the Vercel build log on PRs whose build fails Vercel shows build logs only to members of its team, so contributors saw a red X and a login wall. On vercel.deployment.error this posts the tail of the build log on the PR; on the next successful build the same comment is updated to say the failure is fixed. Fork PRs are skipped so the project-scoped Vercel token is never used on their behalf. --- .github/workflows/vercel-build-report.yml | 52 ++++++ AGENTS.md | 1 + dev/report-vercel-build.mjs | 198 ++++++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 .github/workflows/vercel-build-report.yml create mode 100644 dev/report-vercel-build.mjs diff --git a/.github/workflows/vercel-build-report.yml b/.github/workflows/vercel-build-report.yml new file mode 100644 index 000000000..576e3476b --- /dev/null +++ b/.github/workflows/vercel-build-report.yml @@ -0,0 +1,52 @@ +name: Vercel build report + +# Vercel only shows build logs to members of its team. When a PR's Vercel +# build fails, this comments the end of the build log on the PR; when a later +# revision builds, the comment is updated to say so. +# +# GitHub only delivers repository_dispatch to the workflow file on the default +# branch, so workflow_dispatch takes the same payload fields as inputs for +# testing before merge and for re-running on a PR by hand: +# gh workflow run vercel-build-report.yml --ref \ +# -f id=dpl_... -f state=error -f sha= +on: + repository_dispatch: + types: [vercel.deployment.error, vercel.deployment.success] + workflow_dispatch: + inputs: + id: + description: Vercel deployment ID (client_payload.id) + required: true + state: + description: Deployment state (client_payload.state.type) + required: true + type: choice + options: [error, success] + sha: + description: Full commit SHA of the PR head (client_payload.git.sha) + required: true + +permissions: + contents: read + pull-requests: write + +jobs: + report: + if: github.event.client_payload.environment != 'production' + runs-on: ubuntu-latest + steps: + - name: Check out dev/report-vercel-build.mjs + uses: actions/checkout@v4 + with: + sparse-checkout: dev/report-vercel-build.mjs + sparse-checkout-cone-mode: false + + - name: Comment on the pull request + env: + GH_TOKEN: ${{ github.token }} + DEPLOYMENT_ID: ${{ github.event.client_payload.id || inputs.id }} + DEPLOYMENT_STATE: ${{ github.event.client_payload.state.type || inputs.state }} + COMMIT_SHA: ${{ github.event.client_payload.git.sha || inputs.sha }} + # Scoped to the sourcegraph-docs project, so it needs no team ID + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: node dev/report-vercel-build.mjs diff --git a/AGENTS.md b/AGENTS.md index 580711718..96bcf7879 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ - **Checks**: `npm run check` runs every `dev/check-*.mjs` (links, filenames, images); `npm run build` runs them first, so any finding fails a deploy - **Check links**: `npm run check -- links --check-anchors --check-self-links` (CI comments on PRs that break links; see `dev/check-links.mjs`; the build runs it without flags, so only dead page links fail a deploy). When moving a page or renaming a heading, update every link to it; a redirect in `src/data/redirects.ts` does not satisfy the check. Link to this site with relative paths (`/admin/config/site-config`), never `https://sourcegraph.com/docs/…` or `https://docs.sourcegraph.com/…`. To also probe the external links you added: `npm run check -- links --check-anchors --check-self-links --check-external --diff <(git diff -U0 origin/main)` - **Prove changed links resolve on a deploy**: `node dev/verify-links-live.mjs --site ` prints a Markdown table for the PR description +- **Vercel build failures**: Vercel shows build logs only to its team members, so `.github/workflows/vercel-build-report.yml` comments the log tail on the PR (see `dev/report-vercel-build.mjs`). 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 ## AI Chat Integration diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs new file mode 100644 index 000000000..3f9f16f10 --- /dev/null +++ b/dev/report-vercel-build.mjs @@ -0,0 +1,198 @@ +#!/usr/bin/env node + +/** + * Reports a failed Vercel build on its pull request, since Vercel only shows + * build logs to members of the Vercel team. When a later revision builds, the + * same comment is updated to say so. + * + * Usage: node dev/report-vercel-build.mjs [--dry-run] + * + * Requires DEPLOYMENT_ID, DEPLOYMENT_STATE (error or success), COMMIT_SHA, + * GH_TOKEN and GITHUB_REPOSITORY. A failed build also needs VERCEL_TOKEN, and + * VERCEL_TEAM_ID unless the token is scoped to the project. + * With --dry-run the comment is printed instead of posted. + */ + +const DRY_RUN = process.argv.includes('--dry-run'); +const MAX_LOG_LINES = 100; +const MAX_LOG_CHARS = 30_000; + +const API_URL = process.env.GITHUB_API_URL ?? 'https://api.github.com'; +const REPOSITORY = process.env.GITHUB_REPOSITORY; +const {DEPLOYMENT_ID, DEPLOYMENT_STATE, COMMIT_SHA} = process.env; + +const MARKER = ''; + +async function fetchJson(url, headers) { + const response = await fetch(url, {headers}); + if (!response.ok) { + throw new Error( + `GET ${url} failed: ${response.status} ${await response.text()}` + ); + } + return response.json(); +} + +async function github(method, route, body) { + const response = await fetch(`${API_URL}${route}`, { + method, + headers: { + authorization: `Bearer ${process.env.GH_TOKEN}`, + accept: 'application/vnd.github+json', + 'x-github-api-version': '2022-11-28', + ...(body && {'content-type': 'application/json'}) + }, + body: body && JSON.stringify(body) + }); + if (!response.ok) { + throw new Error( + `${method} ${route} failed: ${response.status} ${await response.text()}` + ); + } + return response.json(); +} + +async function githubList(route) { + const items = []; + for (let page = 1; ; page++) { + const batch = await github('GET', `${route}?per_page=100&page=${page}`); + items.push(...batch); + if (batch.length < 100) { + return items; + } + } +} + +// The dispatch payload has no PR number; look it up from the commit. A stale +// event for a commit the PR has moved past is ignored. Fork PRs are ignored +// too, so the Vercel token is only ever used for commits by people who can +// already push to this repository. +async function findPullRequest() { + const pulls = await github( + 'GET', + `/repos/${REPOSITORY}/commits/${COMMIT_SHA}/pulls` + ); + const pull = pulls.find( + pull => pull.state === 'open' && pull.head.sha === COMMIT_SHA + ); + if (pull && pull.head.repo.full_name !== REPOSITORY) { + console.log(`PR #${pull.number} is from a fork; not reporting`); + return undefined; + } + return pull; +} + +// Build log lines, oldest first. Vercel keeps them as events; only the ones +// with text are log lines. +async function fetchBuildLog() { + const url = new URL( + `https://api.vercel.com/v3/deployments/${DEPLOYMENT_ID}/events` + ); + url.searchParams.set('limit', '-1'); + url.searchParams.set('direction', 'forward'); + if (process.env.VERCEL_TEAM_ID) { + url.searchParams.set('teamId', process.env.VERCEL_TEAM_ID); + } + const events = await fetchJson(url, { + authorization: `Bearer ${process.env.VERCEL_TOKEN}` + }); + return events + .map(event => event.payload?.text ?? event.text) + .filter(text => typeof text === 'string') + .flatMap(text => text.replace(/\n$/, '').split('\n')); +} + +// The failure is at the end of the log; keep the tail within GitHub's comment +// size limit. A four-backtick fence so lines containing ``` cannot break out. +function failureBody(logLines) { + let tail = logLines.slice(-MAX_LOG_LINES); + while (tail.length > 1 && tail.join('\n').length > MAX_LOG_CHARS) { + tail = tail.slice(1); + } + const omitted = logLines.length - tail.length; + return [ + MARKER, + '### ❌ The Vercel build failed for this PR', + '', + 'Vercel only shows build logs to members of its team, so here is the end of the log.', + 'Run `npm run build` locally to reproduce.', + '', + '
', + `Build log${omitted > 0 ? ` (last ${tail.length} of ${logLines.length} lines)` : ''}`, + '', + '````', + ...tail, + '````', + '', + '
', + '' + ].join('\n'); +} + +async function main() { + for (const name of [ + 'DEPLOYMENT_ID', + 'DEPLOYMENT_STATE', + 'COMMIT_SHA', + 'GH_TOKEN', + 'GITHUB_REPOSITORY' + ]) { + if (!process.env[name]) { + throw new Error(`Missing required environment variable ${name}`); + } + } + if (!['error', 'success'].includes(DEPLOYMENT_STATE)) { + throw new Error(`Unexpected DEPLOYMENT_STATE ${DEPLOYMENT_STATE}`); + } + + const pull = await findPullRequest(); + if (!pull) { + console.log(`No open PR with head ${COMMIT_SHA}; nothing to do`); + return; + } + + const comments = await githubList( + `/repos/${REPOSITORY}/issues/${pull.number}/comments` + ); + const existing = comments.find(comment => comment.body.startsWith(MARKER)); + + // Comment only when the build failed, or an earlier failure is resolved + let body; + if (DEPLOYMENT_STATE === 'error') { + if (!process.env.VERCEL_TOKEN) { + throw new Error('VERCEL_TOKEN is required to read the build log'); + } + body = failureBody(await fetchBuildLog()); + } else if (existing) { + body = `${MARKER}\n### ✅ The Vercel build that failed on an earlier revision of this PR passes\n`; + } else { + console.log(`PR #${pull.number} has no failed build to resolve`); + return; + } + + if (DRY_RUN) { + console.log( + `[dry-run] would ${existing ? 'update' : 'create'} comment on PR #${pull.number}:\n` + ); + console.log(body); + } else if (existing) { + console.log(`Updating comment ${existing.id} on PR #${pull.number}`); + await github( + 'PATCH', + `/repos/${REPOSITORY}/issues/comments/${existing.id}`, + {body} + ); + } else { + console.log(`Commenting on PR #${pull.number}`); + await github( + 'POST', + `/repos/${REPOSITORY}/issues/${pull.number}/comments`, + {body} + ); + } +} + +main().catch(error => { + console.error(error); + process.exit(2); +}); From 23bdf69e82c8a5b6a139ad317ccfadc1694bacea Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:41:23 -0600 Subject: [PATCH 02/10] vercel-build-report: report on every open PR at the commit; workflow_dispatch needs the file on main Two open PRs at the same head SHA got one comment on whichever PR the commits/{sha}/pulls API listed first. A deployment belongs to a commit, so comment on each open PR at that head, fetching the build log once. GitHub only resolves workflow_dispatch for workflows on the default branch (gh workflow run --ref 404s before merge), so the header now says to run the script locally until then. Amp-Thread-ID: https://ampcode.com/threads/T-01a08fee-74b4-76dc-aaf9-d1245d68fdc9 Co-authored-by: Amp --- .github/workflows/vercel-build-report.yml | 9 ++-- dev/report-vercel-build.mjs | 55 ++++++++++++++--------- 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/.github/workflows/vercel-build-report.yml b/.github/workflows/vercel-build-report.yml index 576e3476b..743daf74f 100644 --- a/.github/workflows/vercel-build-report.yml +++ b/.github/workflows/vercel-build-report.yml @@ -4,10 +4,11 @@ name: Vercel build report # build fails, this comments the end of the build log on the PR; when a later # revision builds, the comment is updated to say so. # -# GitHub only delivers repository_dispatch to the workflow file on the default -# branch, so workflow_dispatch takes the same payload fields as inputs for -# testing before merge and for re-running on a PR by hand: -# gh workflow run vercel-build-report.yml --ref \ +# GitHub only delivers repository_dispatch (and finds workflow_dispatch +# workflows) once the workflow file is on the default branch, so before merge +# run dev/report-vercel-build.mjs locally instead. After merge, re-run on a PR +# by hand with the same payload fields as inputs: +# gh workflow run vercel-build-report.yml \ # -f id=dpl_... -f state=error -f sha= on: repository_dispatch: diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index 3f9f16f10..85981a32a 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -63,23 +63,26 @@ async function githubList(route) { } } -// The dispatch payload has no PR number; look it up from the commit. A stale -// event for a commit the PR has moved past is ignored. Fork PRs are ignored -// too, so the Vercel token is only ever used for commits by people who can -// already push to this repository. -async function findPullRequest() { +// The dispatch payload has no PR number; look up the PRs from the commit. A +// deployment belongs to a commit, so every open PR at that head gets the +// report. A stale event for a commit a PR has moved past is ignored. Fork PRs +// are ignored too, so the Vercel token is only ever used for commits by +// people who can already push to this repository. +async function findPullRequests() { const pulls = await github( 'GET', `/repos/${REPOSITORY}/commits/${COMMIT_SHA}/pulls` ); - const pull = pulls.find( - pull => pull.state === 'open' && pull.head.sha === COMMIT_SHA - ); - if (pull && pull.head.repo.full_name !== REPOSITORY) { - console.log(`PR #${pull.number} is from a fork; not reporting`); - return undefined; - } - return pull; + return pulls.filter(pull => { + if (pull.state !== 'open' || pull.head.sha !== COMMIT_SHA) { + return false; + } + if (pull.head.repo.full_name !== REPOSITORY) { + console.log(`PR #${pull.number} is from a fork; not reporting`); + return false; + } + return true; + }); } // Build log lines, oldest first. Vercel keeps them as events; only the ones @@ -145,24 +148,34 @@ async function main() { throw new Error(`Unexpected DEPLOYMENT_STATE ${DEPLOYMENT_STATE}`); } - const pull = await findPullRequest(); - if (!pull) { + const pulls = await findPullRequests(); + if (pulls.length === 0) { console.log(`No open PR with head ${COMMIT_SHA}; nothing to do`); return; } + let logLines; + if (DEPLOYMENT_STATE === 'error') { + if (!process.env.VERCEL_TOKEN) { + throw new Error('VERCEL_TOKEN is required to read the build log'); + } + logLines = await fetchBuildLog(); + } + for (const pull of pulls) { + await report(pull, logLines); + } +} + +// Comment only when the build failed, or an earlier failure is resolved +async function report(pull, logLines) { const comments = await githubList( `/repos/${REPOSITORY}/issues/${pull.number}/comments` ); const existing = comments.find(comment => comment.body.startsWith(MARKER)); - // Comment only when the build failed, or an earlier failure is resolved let body; - if (DEPLOYMENT_STATE === 'error') { - if (!process.env.VERCEL_TOKEN) { - throw new Error('VERCEL_TOKEN is required to read the build log'); - } - body = failureBody(await fetchBuildLog()); + if (logLines) { + body = failureBody(logLines); } else if (existing) { body = `${MARKER}\n### ✅ The Vercel build that failed on an earlier revision of this PR passes\n`; } else { From 70059d421ca8936ede43956f321fb15c1ccd8107 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:46:51 -0600 Subject: [PATCH 03/10] ci/vercel-build-report: Reword the failure comment Amp-Thread-ID: https://ampcode.com/threads/T-01a09014-dfa8-740c-95b4-9e28c43cae51 Co-authored-by: Amp --- dev/report-vercel-build.mjs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index 85981a32a..9b6aa6cbe 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -117,11 +117,10 @@ function failureBody(logLines) { MARKER, '### ❌ The Vercel build failed for this PR', '', - 'Vercel only shows build logs to members of its team, so here is the end of the log.', - 'Run `npm run build` locally to reproduce.', + `Vercel paywalls build logs to authorized users in its web UI, so we tailed the last ${tail.length} lines of the build log for you here.`, '', '
', - `Build log${omitted > 0 ? ` (last ${tail.length} of ${logLines.length} lines)` : ''}`, + `Build log${omitted > 0 ? ` (${omitted} earlier lines omitted)` : ''}`, '', '````', ...tail, From 94c00441d1cd4952ac36bf5e54920e9daea5fa3a Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:01:42 -0600 Subject: [PATCH 04/10] ci/vercel-build-report: Attach the full log as an artifact when the comment cannot hold it The comment says how many lines the log has and that the last 100 are shown, matching tail -n 100. When that leaves lines out, the full log is uploaded as a workflow artifact and linked from the comment; the artifact ID rides in the comment marker so the run that reports the build passing deletes it. Amp-Thread-ID: https://ampcode.com/threads/T-01a09014-dfa8-740c-95b4-9e28c43cae51 Co-authored-by: Amp --- .github/workflows/vercel-build-report.yml | 41 ++++-- dev/report-vercel-build.mjs | 157 +++++++++++++++++----- 2 files changed, 153 insertions(+), 45 deletions(-) diff --git a/.github/workflows/vercel-build-report.yml b/.github/workflows/vercel-build-report.yml index 743daf74f..ce1add6f7 100644 --- a/.github/workflows/vercel-build-report.yml +++ b/.github/workflows/vercel-build-report.yml @@ -1,8 +1,10 @@ name: Vercel build report # Vercel only shows build logs to members of its team. When a PR's Vercel -# build fails, this comments the end of the build log on the PR; when a later -# revision builds, the comment is updated to say so. +# build fails, this comments the end of the build log on the PR, with the +# full log as a workflow artifact when the comment cannot hold it all; when a +# later revision builds, the comment is updated to say so and the artifact +# is deleted. # # GitHub only delivers repository_dispatch (and finds workflow_dispatch # workflows) once the workflow file is on the default branch, so before merge @@ -30,6 +32,15 @@ on: permissions: contents: read pull-requests: write + # To delete the full-log artifact once the build passes + actions: write + +env: + DEPLOYMENT_ID: ${{ github.event.client_payload.id || inputs.id }} + DEPLOYMENT_STATE: ${{ github.event.client_payload.state.type || inputs.state }} + COMMIT_SHA: ${{ github.event.client_payload.git.sha || inputs.sha }} + GH_TOKEN: ${{ github.token }} + LOG_FILE: ${{ github.workspace }}/vercel-build.log jobs: report: @@ -42,12 +53,26 @@ jobs: sparse-checkout: dev/report-vercel-build.mjs sparse-checkout-cone-mode: false - - name: Comment on the pull request + - name: Fetch the build log from Vercel + # Vercel is only contacted when the build failed + if: env.DEPLOYMENT_STATE == 'error' + id: log env: - GH_TOKEN: ${{ github.token }} - DEPLOYMENT_ID: ${{ github.event.client_payload.id || inputs.id }} - DEPLOYMENT_STATE: ${{ github.event.client_payload.state.type || inputs.state }} - COMMIT_SHA: ${{ github.event.client_payload.git.sha || inputs.sha }} # Scoped to the sourcegraph-docs project, so it needs no team ID VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - run: node dev/report-vercel-build.mjs + run: node dev/report-vercel-build.mjs fetch-log "$LOG_FILE" + + - name: Attach the full log when the comment cannot hold it all + if: steps.log.outputs.truncated == 'true' + id: artifact + uses: actions/upload-artifact@v4 + with: + name: vercel-build-log-${{ env.COMMIT_SHA }} + path: ${{ env.LOG_FILE }} + retention-days: 30 + + - name: Comment on the pull request + env: + ARTIFACT_ID: ${{ steps.artifact.outputs.artifact-id }} + ARTIFACT_URL: ${{ steps.artifact.outputs.artifact-url }} + run: node dev/report-vercel-build.mjs comment "$LOG_FILE" diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index 9b6aa6cbe..a9fc6609c 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -5,23 +5,40 @@ * build logs to members of the Vercel team. When a later revision builds, the * same comment is updated to say so. * - * Usage: node dev/report-vercel-build.mjs [--dry-run] + * Usage: + * node dev/report-vercel-build.mjs fetch-log + * node dev/report-vercel-build.mjs comment [--dry-run] * - * Requires DEPLOYMENT_ID, DEPLOYMENT_STATE (error or success), COMMIT_SHA, - * GH_TOKEN and GITHUB_REPOSITORY. A failed build also needs VERCEL_TOKEN, and - * VERCEL_TEAM_ID unless the token is scoped to the project. - * With --dry-run the comment is printed instead of posted. + * fetch-log writes the build log to , and `truncated` to GITHUB_OUTPUT, + * so the workflow can upload the full log as an artifact when the comment + * cannot hold all of it. It needs VERCEL_TOKEN, and VERCEL_TEAM_ID unless the + * token is scoped to the project. + * + * comment posts the tail of , linking the artifact from ARTIFACT_ID and + * ARTIFACT_URL when set, and deletes the artifact an earlier comment linked. + * With --dry-run the comment is printed instead, and nothing is deleted. + * + * Both need DEPLOYMENT_ID, DEPLOYMENT_STATE (error or success), COMMIT_SHA, + * GH_TOKEN and GITHUB_REPOSITORY. */ +import {appendFileSync, readFileSync, writeFileSync} from 'fs'; + +const [command, logFile] = process.argv + .slice(2) + .filter(argument => !argument.startsWith('--')); const DRY_RUN = process.argv.includes('--dry-run'); const MAX_LOG_LINES = 100; const MAX_LOG_CHARS = 30_000; +const ARTIFACT_RETENTION_DAYS = 30; const API_URL = process.env.GITHUB_API_URL ?? 'https://api.github.com'; const REPOSITORY = process.env.GITHUB_REPOSITORY; const {DEPLOYMENT_ID, DEPLOYMENT_STATE, COMMIT_SHA} = process.env; -const MARKER = ''; +// The artifact ID rides along in the marker so a later run can delete it +const MARKER = '/; async function fetchJson(url, headers) { const response = await fetch(url, {headers}); @@ -49,7 +66,7 @@ async function github(method, route, body) { `${method} ${route} failed: ${response.status} ${await response.text()}` ); } - return response.json(); + return response.status === 204 ? undefined : response.json(); } async function githubList(route) { @@ -73,7 +90,7 @@ async function findPullRequests() { 'GET', `/repos/${REPOSITORY}/commits/${COMMIT_SHA}/pulls` ); - return pulls.filter(pull => { + const open = pulls.filter(pull => { if (pull.state !== 'open' || pull.head.sha !== COMMIT_SHA) { return false; } @@ -83,6 +100,10 @@ async function findPullRequests() { } return true; }); + if (open.length === 0) { + console.log(`No open PR with head ${COMMIT_SHA}; nothing to do`); + } + return open; } // Build log lines, oldest first. Vercel keeps them as events; only the ones @@ -105,22 +126,48 @@ async function fetchBuildLog() { .flatMap(text => text.replace(/\n$/, '').split('\n')); } -// The failure is at the end of the log; keep the tail within GitHub's comment -// size limit. A four-backtick fence so lines containing ``` cannot break out. -function failureBody(logLines) { +// The failure is at the end of the log; keep the tail within GitHub's +// comment size limit +function tailOf(logLines) { let tail = logLines.slice(-MAX_LOG_LINES); while (tail.length > 1 && tail.join('\n').length > MAX_LOG_CHARS) { tail = tail.slice(1); } - const omitted = logLines.length - tail.length; + return tail; +} + +async function fetchLog() { + if (!process.env.VERCEL_TOKEN) { + throw new Error('VERCEL_TOKEN is required to read the build log'); + } + if ((await findPullRequests()).length === 0) { + return; + } + const logLines = await fetchBuildLog(); + writeFileSync(logFile, logLines.join('\n') + '\n'); + const truncated = tailOf(logLines).length < logLines.length; + console.log( + `Wrote ${logLines.length} log lines to ${logFile}${truncated ? '; the comment will show the tail' : ''}` + ); + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `truncated=${truncated}\n`); + } +} + +// A four-backtick fence so lines containing ``` cannot break out of the block +function failureBody(logLines, artifact) { + const tail = tailOf(logLines); + const fullLog = artifact + ? `The full log is ${logLines.length} lines, attached as a [workflow artifact](${artifact.url}); downloading it needs a GitHub login, and it expires in ${ARTIFACT_RETENTION_DAYS} days.` + : `The full log is ${logLines.length} lines.`; return [ - MARKER, + `${MARKER}${artifact ? ` artifact=${artifact.id}` : ''} -->`, '### ❌ The Vercel build failed for this PR', '', - `Vercel paywalls build logs to authorized users in its web UI, so we tailed the last ${tail.length} lines of the build log for you here.`, + `Vercel paywalls build logs to authorized users in its web UI, so we tailed the last ${MAX_LOG_LINES} lines of the build log for you here. ${fullLog}`, '', '
', - `Build log${omitted > 0 ? ` (${omitted} earlier lines omitted)` : ''}`, + 'Build log', '', '````', ...tail, @@ -131,34 +178,29 @@ function failureBody(logLines) { ].join('\n'); } -async function main() { - for (const name of [ - 'DEPLOYMENT_ID', - 'DEPLOYMENT_STATE', - 'COMMIT_SHA', - 'GH_TOKEN', - 'GITHUB_REPOSITORY' - ]) { - if (!process.env[name]) { - throw new Error(`Missing required environment variable ${name}`); - } +async function deleteArtifact(id) { + console.log(`${DRY_RUN ? '[dry-run] ' : ''}Deleting artifact ${id}`); + if (DRY_RUN) { + return; } - if (!['error', 'success'].includes(DEPLOYMENT_STATE)) { - throw new Error(`Unexpected DEPLOYMENT_STATE ${DEPLOYMENT_STATE}`); + try { + await github('DELETE', `/repos/${REPOSITORY}/actions/artifacts/${id}`); + } catch (error) { + // Already expired or deleted + if (!error.message.includes(' 404 ')) { + throw error; + } } +} +async function comment() { const pulls = await findPullRequests(); if (pulls.length === 0) { - console.log(`No open PR with head ${COMMIT_SHA}; nothing to do`); return; } - let logLines; if (DEPLOYMENT_STATE === 'error') { - if (!process.env.VERCEL_TOKEN) { - throw new Error('VERCEL_TOKEN is required to read the build log'); - } - logLines = await fetchBuildLog(); + logLines = readFileSync(logFile, 'utf8').replace(/\n$/, '').split('\n'); } for (const pull of pulls) { await report(pull, logLines); @@ -170,18 +212,29 @@ async function report(pull, logLines) { const comments = await githubList( `/repos/${REPOSITORY}/issues/${pull.number}/comments` ); - const existing = comments.find(comment => comment.body.startsWith(MARKER)); + const existing = comments.find(comment => + MARKER_PATTERN.test(comment.body) + ); + const previousArtifact = existing?.body.match(MARKER_PATTERN)[1]; let body; if (logLines) { - body = failureBody(logLines); + const {ARTIFACT_ID, ARTIFACT_URL} = process.env; + body = failureBody( + logLines, + ARTIFACT_ID && {id: ARTIFACT_ID, url: ARTIFACT_URL} + ); } else if (existing) { - body = `${MARKER}\n### ✅ The Vercel build that failed on an earlier revision of this PR passes\n`; + body = `${MARKER} -->\n### ✅ The Vercel build that failed on an earlier revision of this PR passes\n`; } else { console.log(`PR #${pull.number} has no failed build to resolve`); return; } + if (previousArtifact) { + await deleteArtifact(previousArtifact); + } + if (DRY_RUN) { console.log( `[dry-run] would ${existing ? 'update' : 'create'} comment on PR #${pull.number}:\n` @@ -204,6 +257,36 @@ async function report(pull, logLines) { } } +async function main() { + for (const name of [ + 'DEPLOYMENT_ID', + 'DEPLOYMENT_STATE', + 'COMMIT_SHA', + 'GH_TOKEN', + 'GITHUB_REPOSITORY' + ]) { + if (!process.env[name]) { + throw new Error(`Missing required environment variable ${name}`); + } + } + if (!['error', 'success'].includes(DEPLOYMENT_STATE)) { + throw new Error(`Unexpected DEPLOYMENT_STATE ${DEPLOYMENT_STATE}`); + } + if (!logFile) { + throw new Error( + 'Usage: node dev/report-vercel-build.mjs fetch-log|comment ' + ); + } + + if (command === 'fetch-log') { + await fetchLog(); + } else if (command === 'comment') { + await comment(); + } else { + throw new Error(`Unknown command ${command}; use fetch-log or comment`); + } +} + main().catch(error => { console.error(error); process.exit(2); From dc279c3a15f1421c77307a7b69f062da52c00e6e Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:41:42 -0600 Subject: [PATCH 05/10] ci/vercel-build-report: Plain wording when the comment holds the whole log --- dev/report-vercel-build.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index a9fc6609c..c254b1609 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -157,14 +157,16 @@ async function fetchLog() { // A four-backtick fence so lines containing ``` cannot break out of the block function failureBody(logLines, artifact) { const tail = tailOf(logLines); - const fullLog = artifact - ? `The full log is ${logLines.length} lines, attached as a [workflow artifact](${artifact.url}); downloading it needs a GitHub login, and it expires in ${ARTIFACT_RETENTION_DAYS} days.` - : `The full log is ${logLines.length} lines.`; + const intro = + 'Vercel paywalls build logs to authorized users in its web UI, so'; + const message = artifact + ? `${intro} we tailed the last ${tail.length} lines of the build log for you here. The full log is ${logLines.length} lines, attached as a [workflow artifact](${artifact.url}); downloading it needs a GitHub login, and it expires in ${ARTIFACT_RETENTION_DAYS} days.` + : `${intro} here is the build log.`; return [ `${MARKER}${artifact ? ` artifact=${artifact.id}` : ''} -->`, '### ❌ The Vercel build failed for this PR', '', - `Vercel paywalls build logs to authorized users in its web UI, so we tailed the last ${MAX_LOG_LINES} lines of the build log for you here. ${fullLog}`, + message, '', '
', 'Build log', From 72e18fc2b5beb4d3afb1e879bd0ff585411ceca2 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:01:30 -0600 Subject: [PATCH 06/10] ci/vercel-build-report: Comment on the PR Vercel built the deployment for A deployment is built for one branch, and Vercel records that PR in the deployment's meta.githubPrId. fetch-log reads it and hands it to the comment step as pull_request, so two PRs at the same commit no longer both get the log. The success path stays off Vercel and keeps the commit lookup, since it only updates comments that already exist. The open / same-repo / head-is-this-commit checks apply either way. Amp-Thread-ID: https://ampcode.com/threads/T-01a08fee-74b4-76dc-aaf9-d1245d68fdc9 Co-authored-by: Amp --- .github/workflows/vercel-build-report.yml | 1 + dev/report-vercel-build.mjs | 60 +++++++++++++++++------ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/.github/workflows/vercel-build-report.yml b/.github/workflows/vercel-build-report.yml index ce1add6f7..41739d34f 100644 --- a/.github/workflows/vercel-build-report.yml +++ b/.github/workflows/vercel-build-report.yml @@ -73,6 +73,7 @@ jobs: - name: Comment on the pull request env: + PR_NUMBER: ${{ steps.log.outputs.pull_request }} ARTIFACT_ID: ${{ steps.artifact.outputs.artifact-id }} ARTIFACT_URL: ${{ steps.artifact.outputs.artifact-url }} run: node dev/report-vercel-build.mjs comment "$LOG_FILE" diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index c254b1609..950177f79 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -9,12 +9,14 @@ * node dev/report-vercel-build.mjs fetch-log * node dev/report-vercel-build.mjs comment [--dry-run] * - * fetch-log writes the build log to , and `truncated` to GITHUB_OUTPUT, + * fetch-log writes the build log to , and to GITHUB_OUTPUT `truncated`, * so the workflow can upload the full log as an artifact when the comment - * cannot hold all of it. It needs VERCEL_TOKEN, and VERCEL_TEAM_ID unless the - * token is scoped to the project. + * cannot hold all of it, and `pull_request`, the PR Vercel built the + * deployment for. It needs VERCEL_TOKEN, and VERCEL_TEAM_ID unless the token + * is scoped to the project. * - * comment posts the tail of , linking the artifact from ARTIFACT_ID and + * comment posts the tail of on PR_NUMBER, or on the open PRs at + * COMMIT_SHA when unset, linking the artifact from ARTIFACT_ID and * ARTIFACT_URL when set, and deletes the artifact an earlier comment linked. * With --dry-run the comment is printed instead, and nothing is deleted. * @@ -80,16 +82,38 @@ async function githubList(route) { } } -// The dispatch payload has no PR number; look up the PRs from the commit. A -// deployment belongs to a commit, so every open PR at that head gets the -// report. A stale event for a commit a PR has moved past is ignored. Fork PRs -// are ignored too, so the Vercel token is only ever used for commits by -// people who can already push to this repository. -async function findPullRequests() { - const pulls = await github( - 'GET', - `/repos/${REPOSITORY}/commits/${COMMIT_SHA}/pulls` +// Vercel records which PR a deployment was built for. Empty when the branch +// was deployed before its PR was opened. +async function fetchDeploymentPullRequestNumber() { + const url = new URL( + `https://api.vercel.com/v13/deployments/${DEPLOYMENT_ID}` ); + if (process.env.VERCEL_TEAM_ID) { + url.searchParams.set('teamId', process.env.VERCEL_TEAM_ID); + } + const deployment = await fetchJson(url, { + authorization: `Bearer ${process.env.VERCEL_TOKEN}` + }); + return deployment.meta?.githubPrId; +} + +// The dispatch payload has no PR number. The failure path gets it from the +// deployment; the success path only knows the commit, so it looks up the PRs +// at that head. Either way a stale event for a commit a PR has moved past is +// ignored, and so are fork PRs, so the Vercel token is only ever used for +// commits by people who can already push to this repository. +async function findPullRequests(pullRequestNumber) { + const pulls = pullRequestNumber + ? [ + await github( + 'GET', + `/repos/${REPOSITORY}/pulls/${pullRequestNumber}` + ) + ] + : await github( + 'GET', + `/repos/${REPOSITORY}/commits/${COMMIT_SHA}/pulls` + ); const open = pulls.filter(pull => { if (pull.state !== 'open' || pull.head.sha !== COMMIT_SHA) { return false; @@ -140,7 +164,8 @@ async function fetchLog() { if (!process.env.VERCEL_TOKEN) { throw new Error('VERCEL_TOKEN is required to read the build log'); } - if ((await findPullRequests()).length === 0) { + const pullRequestNumber = await fetchDeploymentPullRequestNumber(); + if ((await findPullRequests(pullRequestNumber)).length === 0) { return; } const logLines = await fetchBuildLog(); @@ -150,7 +175,10 @@ async function fetchLog() { `Wrote ${logLines.length} log lines to ${logFile}${truncated ? '; the comment will show the tail' : ''}` ); if (process.env.GITHUB_OUTPUT) { - appendFileSync(process.env.GITHUB_OUTPUT, `truncated=${truncated}\n`); + appendFileSync( + process.env.GITHUB_OUTPUT, + `truncated=${truncated}\npull_request=${pullRequestNumber ?? ''}\n` + ); } } @@ -196,7 +224,7 @@ async function deleteArtifact(id) { } async function comment() { - const pulls = await findPullRequests(); + const pulls = await findPullRequests(process.env.PR_NUMBER); if (pulls.length === 0) { return; } From 96cdef146e4257970a319833c320241611065513 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:27:42 -0600 Subject: [PATCH 07/10] ci/vercel-build-report: Check GitHub for an open same-repo PR before using the Vercel token fetch-log asked Vercel which PR the deployment was for, then checked GitHub that an open PR from this repository is at the commit. Swap the order, so a dispatch for a fork PR or a stale commit never spends the Vercel token. Amp-Thread-ID: https://ampcode.com/threads/T-01a08fee-74b4-76dc-aaf9-d1245d68fdc9 Co-authored-by: Amp --- dev/report-vercel-build.mjs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index 950177f79..cb190ad25 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -15,10 +15,12 @@ * deployment for. It needs VERCEL_TOKEN, and VERCEL_TEAM_ID unless the token * is scoped to the project. * - * comment posts the tail of on PR_NUMBER, or on the open PRs at - * COMMIT_SHA when unset, linking the artifact from ARTIFACT_ID and - * ARTIFACT_URL when set, and deletes the artifact an earlier comment linked. - * With --dry-run the comment is printed instead, and nothing is deleted. + * comment posts the tail of on PR_NUMBER, the PR Vercel built the + * deployment for. When unset (the success path, or a deployment Vercel + * recorded no PR for) it falls back to every open PR at COMMIT_SHA. It links + * the artifact from ARTIFACT_ID and ARTIFACT_URL when set, and deletes the + * artifact an earlier comment linked. With --dry-run the comment is printed + * instead, and nothing is deleted. * * Both need DEPLOYMENT_ID, DEPLOYMENT_STATE (error or success), COMMIT_SHA, * GH_TOKEN and GITHUB_REPOSITORY. @@ -100,8 +102,7 @@ async function fetchDeploymentPullRequestNumber() { // The dispatch payload has no PR number. The failure path gets it from the // deployment; the success path only knows the commit, so it looks up the PRs // at that head. Either way a stale event for a commit a PR has moved past is -// ignored, and so are fork PRs, so the Vercel token is only ever used for -// commits by people who can already push to this repository. +// ignored, and so are fork PRs. async function findPullRequests(pullRequestNumber) { const pulls = pullRequestNumber ? [ @@ -118,7 +119,8 @@ async function findPullRequests(pullRequestNumber) { if (pull.state !== 'open' || pull.head.sha !== COMMIT_SHA) { return false; } - if (pull.head.repo.full_name !== REPOSITORY) { + // head.repo is null when the fork was deleted + if (pull.head.repo?.full_name !== REPOSITORY) { console.log(`PR #${pull.number} is from a fork; not reporting`); return false; } @@ -164,10 +166,13 @@ async function fetchLog() { if (!process.env.VERCEL_TOKEN) { throw new Error('VERCEL_TOKEN is required to read the build log'); } - const pullRequestNumber = await fetchDeploymentPullRequestNumber(); - if ((await findPullRequests(pullRequestNumber)).length === 0) { + // Ask GitHub before Vercel, so the Vercel token is only ever used for a + // commit that an open PR from this repository is at, i.e. pushed by + // someone who can already push here + if ((await findPullRequests()).length === 0) { return; } + const pullRequestNumber = await fetchDeploymentPullRequestNumber(); const logLines = await fetchBuildLog(); writeFileSync(logFile, logLines.join('\n') + '\n'); const truncated = tailOf(logLines).length < logLines.length; From 219f9b747907c29cd3b881be79f7321a9f58f6bf Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:27:42 -0600 Subject: [PATCH 08/10] ci/vercel-build-report: Size the code fence to the log and redact credential shapes A log line of four or more backticks closed the fixed fence and let the rest of the log render as Markdown in the bot comment. Make the fence one backtick longer than the longest run in the tail. The comment and artifact are public and the build gets VERCEL_OIDC_TOKEN, VERCEL_DEPLOYMENT_KEY and VERCEL_ENV_ENC_KEY, so a build that prints its environment would publish them. Redact JWTs, known token prefixes, Bearer values and TOKEN/SECRET/PASSWORD/KEY assignments before the log is written. Amp-Thread-ID: https://ampcode.com/threads/T-01a08fee-74b4-76dc-aaf9-d1245d68fdc9 Co-authored-by: Amp --- dev/report-vercel-build.mjs | 44 +++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index cb190ad25..37a60632b 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -149,7 +149,31 @@ async function fetchBuildLog() { return events .map(event => event.payload?.text ?? event.text) .filter(text => typeof text === 'string') - .flatMap(text => text.replace(/\n$/, '').split('\n')); + .flatMap(text => text.replace(/\n$/, '').split('\n')) + .map(redact); +} + +// Credential shapes a build might print. The comment and artifact are public, +// and the build gets VERCEL_OIDC_TOKEN and friends, so a left-in +// `console.log(process.env)` must not publish them. Not a complete list. +const REDACTIONS = [ + [/\beyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]+/g, '[redacted-jwt]'], + [ + /\b(?:vcp_|gh[pousr]_|github_pat_|sk-|xox[abpr]-)[\w-]{16,}|\bAKIA[0-9A-Z]{16}\b/g, + '[redacted-token]' + ], + [/(\bBearer\s+)\S+/gi, '$1[redacted]'], + [ + /(\w*(?:TOKEN|SECRET|PASSW(?:OR)?D|CREDENTIALS?|API_?KEY|PRIVATE_KEY|ENC_KEY|DEPLOYMENT_KEY)\w*["']?\s*[=:]\s*["']?)\S+/gi, + '$1[redacted]' + ] +]; + +function redact(line) { + return REDACTIONS.reduce( + (text, [pattern, replacement]) => text.replace(pattern, replacement), + line + ); } // The failure is at the end of the log; keep the tail within GitHub's @@ -187,9 +211,21 @@ async function fetchLog() { } } -// A four-backtick fence so lines containing ``` cannot break out of the block +// A fence longer than any run of backticks in the log, so no log line can +// close it and inject Markdown into the comment +function fenceFor(lines) { + const longestRun = Math.max( + 2, + ...lines.flatMap(line => + (line.match(/`+/g) ?? []).map(run => run.length) + ) + ); + return '`'.repeat(longestRun + 1); +} + function failureBody(logLines, artifact) { const tail = tailOf(logLines); + const fence = fenceFor(tail); const intro = 'Vercel paywalls build logs to authorized users in its web UI, so'; const message = artifact @@ -204,9 +240,9 @@ function failureBody(logLines, artifact) { '
', 'Build log', '', - '````', + fence, ...tail, - '````', + fence, '', '
', '' From d854cfe8471a6da2d898f4b4c6f1affa9597fab7 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:55:42 -0600 Subject: [PATCH 09/10] ci/vercel-build-report: Attach the full log to the Vercel app's Slack failure post Looks back 30 minutes in the channel for the "failed to deploy" post for the commit, waiting up to 5 more minutes for it, then uploads the redacted log into its thread. Needs the SLACK_BOT_TOKEN secret and SLACK_CHANNEL_ID variable; skips quietly without them. Amp-Thread-ID: https://ampcode.com/threads/T-01a09292-4b20-771b-bd66-3ca16f7aad07 Co-authored-by: Amp --- .github/workflows/vercel-build-report.yml | 14 ++- AGENTS.md | 2 +- dev/report-vercel-build.mjs | 142 +++++++++++++++++++++- dev/slack-app-vercel-build-report.json | 22 ++++ 4 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 dev/slack-app-vercel-build-report.json diff --git a/.github/workflows/vercel-build-report.yml b/.github/workflows/vercel-build-report.yml index 41739d34f..91a64f6f1 100644 --- a/.github/workflows/vercel-build-report.yml +++ b/.github/workflows/vercel-build-report.yml @@ -4,7 +4,9 @@ name: Vercel build report # build fails, this comments the end of the build log on the PR, with the # full log as a workflow artifact when the comment cannot hold it all; when a # later revision builds, the comment is updated to say so and the artifact -# is deleted. +# is deleted. The full log is also attached to the Vercel Slack app's "failed +# to deploy" post when the SLACK_BOT_TOKEN secret and SLACK_CHANNEL_ID +# variable are set (see dev/slack-app-vercel-build-report.json). # # GitHub only delivers repository_dispatch (and finds workflow_dispatch # workflows) once the workflow file is on the default branch, so before merge @@ -77,3 +79,13 @@ jobs: ARTIFACT_ID: ${{ steps.artifact.outputs.artifact-id }} ARTIFACT_URL: ${{ steps.artifact.outputs.artifact-url }} run: node dev/report-vercel-build.mjs comment "$LOG_FILE" + + - name: Attach the log to the Vercel app's Slack post + if: env.DEPLOYMENT_STATE == 'error' + # The PR comment is the record; a Slack problem must not fail it + continue-on-error: true + env: + PR_NUMBER: ${{ steps.log.outputs.pull_request }} + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ vars.SLACK_CHANNEL_ID }} + run: node dev/report-vercel-build.mjs slack "$LOG_FILE" diff --git a/AGENTS.md b/AGENTS.md index 96bcf7879..674ff4307 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ - **Checks**: `npm run check` runs every `dev/check-*.mjs` (links, filenames, images); `npm run build` runs them first, so any finding fails a deploy - **Check links**: `npm run check -- links --check-anchors --check-self-links` (CI comments on PRs that break links; see `dev/check-links.mjs`; the build runs it without flags, so only dead page links fail a deploy). When moving a page or renaming a heading, update every link to it; a redirect in `src/data/redirects.ts` does not satisfy the check. Link to this site with relative paths (`/admin/config/site-config`), never `https://sourcegraph.com/docs/…` or `https://docs.sourcegraph.com/…`. To also probe the external links you added: `npm run check -- links --check-anchors --check-self-links --check-external --diff <(git diff -U0 origin/main)` - **Prove changed links resolve on a deploy**: `node dev/verify-links-live.mjs --site ` prints a Markdown table for the PR description -- **Vercel build failures**: Vercel shows build logs only to its team members, so `.github/workflows/vercel-build-report.yml` comments the log tail on the PR (see `dev/report-vercel-build.mjs`). 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 +- **Vercel build failures**: Vercel shows build logs only to its team members, so `.github/workflows/vercel-build-report.yml` comments the log tail on the PR (see `dev/report-vercel-build.mjs`). 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. It also attaches the full log to the Vercel Slack app's "failed to deploy" post in `#alerts-vercel-doc-site`, using the `SLACK_BOT_TOKEN` repo secret and `SLACK_CHANNEL_ID` repo variable. The bot is the Slack app in `dev/slack-app-vercel-build-report.json`; to recreate it, paste that manifest at (From a manifest), install it, copy its Bot User OAuth Token into the secret, and `/invite @Vercel build log` to the channel ## AI Chat Integration diff --git a/dev/report-vercel-build.mjs b/dev/report-vercel-build.mjs index 37a60632b..75b24d340 100644 --- a/dev/report-vercel-build.mjs +++ b/dev/report-vercel-build.mjs @@ -8,6 +8,7 @@ * Usage: * node dev/report-vercel-build.mjs fetch-log * node dev/report-vercel-build.mjs comment [--dry-run] + * node dev/report-vercel-build.mjs slack [--dry-run] * * fetch-log writes the build log to , and to GITHUB_OUTPUT `truncated`, * so the workflow can upload the full log as an artifact when the comment @@ -22,7 +23,14 @@ * artifact an earlier comment linked. With --dry-run the comment is printed * instead, and nothing is deleted. * - * Both need DEPLOYMENT_ID, DEPLOYMENT_STATE (error or success), COMMIT_SHA, + * slack uploads into the thread of the Vercel Slack app's "failed to + * deploy" post for the commit in SLACK_CHANNEL_ID, looking back 30 minutes + * and waiting up to 5 more for the post to appear. It needs SLACK_BOT_TOKEN + * (see dev/slack-app-vercel-build-report.json) and does nothing when that or + * SLACK_CHANNEL_ID is unset. With --dry-run the post is found but nothing is + * uploaded. + * + * All need DEPLOYMENT_ID, DEPLOYMENT_STATE (error or success), COMMIT_SHA, * GH_TOKEN and GITHUB_REPOSITORY. */ @@ -35,10 +43,14 @@ const DRY_RUN = process.argv.includes('--dry-run'); const MAX_LOG_LINES = 100; const MAX_LOG_CHARS = 30_000; const ARTIFACT_RETENTION_DAYS = 30; +const SLACK_LOOKBACK_MINUTES = 30; +const SLACK_WAIT_MINUTES = 5; +const SLACK_POLL_SECONDS = 15; const API_URL = process.env.GITHUB_API_URL ?? 'https://api.github.com'; const REPOSITORY = process.env.GITHUB_REPOSITORY; -const {DEPLOYMENT_ID, DEPLOYMENT_STATE, COMMIT_SHA} = process.env; +const {DEPLOYMENT_ID, DEPLOYMENT_STATE, COMMIT_SHA, SLACK_CHANNEL_ID} = + process.env; // The artifact ID rides along in the marker so a later run can delete it const MARKER = '