From 16d546f30785a8cfa510b23ae2b456970d4ebc6c Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 4 Aug 2026 16:33:24 +0800 Subject: [PATCH] Maintain one PR preview history comment --- .github/workflows/ci.yml | 76 ++++++++++++++++++++++++++++++++++------ README.md | 2 +- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 744fb83..0b55f39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,23 +62,77 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | + const marker = ""; + const historyPattern = //; const sha = process.env.COMMIT_SHA; const snapshotUrl = process.env.SNAPSHOT_URL; const stableUrl = process.env.STABLE_URL; - const shortSha = sha.slice(0, 7); - const commitUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/commit/${sha}`; + const issue = { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }; + + const comments = await github.paginate( + github.rest.issues.listComments, + { ...issue, per_page: 100 }, + ); + const existingComment = comments.find( + (comment) => + comment.user?.login === "github-actions[bot]" && + comment.body?.includes(marker), + ); + + let history = []; + const historyMatch = existingComment?.body?.match(historyPattern); + if (historyMatch) { + try { + history = JSON.parse( + Buffer.from(historyMatch[1], "base64").toString("utf8"), + ); + } catch (error) { + core.warning(`Could not decode preview history: ${error.message}`); + } + } + + const currentEntry = { sha, url: snapshotUrl }; + const currentIndex = history.findIndex((entry) => entry.sha === sha); + if (currentIndex === -1) { + history.push(currentEntry); + } else { + history[currentIndex] = currentEntry; + } + + const encodedHistory = Buffer.from( + JSON.stringify(history), + "utf8", + ).toString("base64"); + const commitBaseUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/commit`; + const historyRows = history.map( + (entry) => + `| [\`${entry.sha.slice(0, 7)}\`](${commitBaseUrl}/${entry.sha}) | [Open snapshot](${entry.url}) |`, + ); const body = [ + marker, + ``, "## Cloudflare Pages preview", "", - `- **Latest PR preview:** [${stableUrl}](${stableUrl})`, - `- **Snapshot for [\`${shortSha}\`](${commitUrl}):** [${snapshotUrl}](${snapshotUrl})`, + `**Latest PR preview:** [${stableUrl}](${stableUrl})`, + "", + "| Commit | Immutable snapshot |", + "| --- | --- |", + ...historyRows, "", - "The PR URL always points to the latest deployment; the commit snapshot is immutable.", + "The PR URL always points to the latest deployment. Each commit snapshot is immutable.", ].join("\n"); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); + if (existingComment) { + await github.rest.issues.updateComment({ + owner: issue.owner, + repo: issue.repo, + comment_id: existingComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ ...issue, body }); + } diff --git a/README.md b/README.md index be0db37..9f720a0 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ The Pages workflow deploys every push to `main` and can also be run manually fro ## Pull request previews -Pull requests opened from branches in this repository are deployed to an isolated Cloudflare Pages preview after CI succeeds. Each pull request receives a stable branch alias such as `https://pr-42.display-list-explorer.pages.dev`, while every update also creates an immutable deployment URL. GitHub attaches the resulting URL to the pull request as a deployment and posts both URLs in a commit-specific PR comment, preserving access to earlier snapshots as new commits are pushed. +Pull requests opened from branches in this repository are deployed to an isolated Cloudflare Pages preview after CI succeeds. Each pull request receives a stable branch alias such as `https://pr-42.display-list-explorer.pages.dev`, while every update also creates an immutable deployment URL. GitHub attaches the resulting URL to the pull request as a deployment and maintains a single bot comment containing the stable URL and a commit-by-commit table of immutable snapshots. The workflow expects a `CLOUDFLARE_API_TOKEN` repository secret with Cloudflare Pages edit access and a `CLOUDFLARE_ACCOUNT_ID` repository variable. Pull requests from forks still run the converter and site build, but skip deployment because repository secrets are not exposed to fork workflows.