From 2866fa337227219037b12140c12b1b053e39ca60 Mon Sep 17 00:00:00 2001 From: Stephen Date: Tue, 4 Aug 2026 09:53:48 -0700 Subject: [PATCH] ci: post the Cloudflare preview URL as a PR comment Cloudflare's GitHub App stopped commenting on this repo's PRs after Jul 30 (#171 and #172 got none), while its `Workers Builds` check run kept arriving with the preview URLs in its summary. Read them off the check run and post the comment with our own GITHUB_TOKEN instead. Adapted from tightknit-app's cf-preview-urls.yml, minus the parts that only mattered there: no multi-worker aggregation, no constructed fallback URLs, and the commit-ordering guard replaced by a plain head-SHA equality check. Co-Authored-By: Claude Opus 5 --- .github/workflows/cf-preview-url.yml | 93 ++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/cf-preview-url.yml diff --git a/.github/workflows/cf-preview-url.yml b/.github/workflows/cf-preview-url.yml new file mode 100644 index 0000000..e930da3 --- /dev/null +++ b/.github/workflows/cf-preview-url.yml @@ -0,0 +1,93 @@ +name: Cloudflare Preview URL + +# Cloudflare Workers Builds reports each build as a check run whose summary +# carries the preview URLs. Its GitHub App is also supposed to post those as a +# PR comment, but that stopped happening for this repo; the check run itself +# still arrives, so post the comment ourselves from the data it carries. +# +# GitHub only dispatches `check_run` events to workflow files on the default +# branch, so edits here take effect once merged, not on the PR that makes them. +on: + check_run: + types: [completed] + +jobs: + comment: + name: Post preview URL + # Cloudflare is not the only app posting check runs on this repo. + if: github.event.check_run.app.slug == 'cloudflare-workers-and-pages' + runs-on: ubuntu-latest + timeout-minutes: 5 + # One comment per branch: serialize so two builds finishing together can't + # each decide no comment exists yet and create their own. + concurrency: + group: cf-preview-url-${{ github.event.check_run.check_suite.head_branch }} + permissions: + pull-requests: read + issues: write + + steps: + - uses: actions/github-script@v9 + with: + script: | + const run = context.payload.check_run; + const sha = run.head_sha; + const branch = run.check_suite?.head_branch; + if (!branch) { + console.log('No head branch on the check run, skipping.'); + return; + } + + // Resolve the PR by branch rather than by head SHA: a build that + // finishes after another push has already moved the head still + // needs to find its PR (the staleness guard below handles ordering). + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:${branch}`, + state: 'open' + }); + if (prs.length === 0) { + console.log(`No open PR for branch ${branch}, skipping.`); + return; + } + + const summary = run.output?.summary ?? ''; + const branchUrl = summary.match(/Preview Alias URL:\s*(https:\/\/\S+)/)?.[1]; + const commitUrl = summary.match(/Preview URL:\s*(https:\/\/\S+)/)?.[1]; + + const MARKER = ''; + const status = run.conclusion === 'success' ? '✅ Deployed' : `❌ Build ${run.conclusion}`; + const links = [ + branchUrl ? `[Branch preview](${branchUrl})` : null, + commitUrl ? `[Commit preview](${commitUrl})` : null, + run.details_url ? `[Build logs](${run.details_url})` : null + ].filter(Boolean); + const body = `${MARKER}\n## Cloudflare preview\n\n${status} \`${sha.slice(0, 7)}\` · ${links.join(' · ') || 'no URLs reported'}\n`; + + for (const pr of prs) { + // SHORTCUT: only comment for the PR's current head. A build that + // finishes after a newer push is stale, and the newer build posts + // its own check run moments later. Cheaper than ordering the two + // commits, at the cost of no comment during that gap. + if (sha !== pr.head.sha) { + console.log(`Build ${sha.slice(0, 7)} is behind PR #${pr.number} head, skipping.`); + continue; + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + per_page: 100 + }); + const existing = comments.find((c) => c.body?.includes(MARKER)); + const target = { owner: context.repo.owner, repo: context.repo.repo }; + + if (existing) { + await github.rest.issues.updateComment({ ...target, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ ...target, issue_number: pr.number, body }); + } + console.log(`${existing ? 'Updated' : 'Created'} preview comment on PR #${pr.number}`); + }