diff --git a/cloudflare-pages-preview/README.md b/cloudflare-pages-preview/README.md index 8ce3cba..b42bec8 100644 --- a/cloudflare-pages-preview/README.md +++ b/cloudflare-pages-preview/README.md @@ -11,7 +11,9 @@ and cleaning them up when the PR closes. previews aren't crawled by search engines (optional, on by default) 2. Deploys to Cloudflare Pages under a per-PR branch alias (`pr-` by default) giving a stable URL across force-pushes - 3. Posts or updates a sticky PR comment with the preview URLs + 3. Posts a PR comment with the preview URLs — either updating a sticky + comment in place (`comment-mode: sticky`, default) or minimizing prior + preview comments as OUTDATED and reposting fresh (`comment-mode: repost`) - **`cleanup`** — On PR close: 1. Deletes all Cloudflare Pages deployments for the PR's branch alias @@ -137,6 +139,7 @@ Cloudflare API and GitHub API. | `branch` | | `pr-` | CF Pages branch alias | | `inject-noindex` | | `'true'` | Inject `_headers` + `` noindex | | `comment-marker` | | `` | Sticky-comment identifier | +| `comment-mode` | | `sticky` | `sticky` updates the existing marker comment in place. `repost` minimizes all prior marker comments as **OUTDATED** (GitHub's native hide) and posts a fresh one. | | `comment-title` | | `Preview Deployed` | Heading in PR comment | | `github-token` | | `${{ github.token }}` | Token for PR commenting | | `pr-number` | | `${{ github.event.pull_request.number }}` | PR number to comment on — override when running on `workflow_dispatch` or other triggers without `payload.pull_request` | @@ -151,6 +154,7 @@ Cloudflare API and GitHub API. | `account-id` | ✅ | — | Cloudflare account ID | | `branch` | | `pr-` | CF Pages branch alias to delete | | `comment-marker` | | `` | Sticky-comment identifier | +| `comment-mode` | | `sticky` | Must match `deploy`'s `comment-mode`. In `repost` mode, minimizes all marker comments and posts a final cleanup comment. | | `github-token` | | `${{ github.token }}` | Token for PR commenting | | `pr-number` | | `${{ github.event.pull_request.number }}` | PR number to update the sticky comment on — override when running on `workflow_dispatch` or other triggers without `payload.pull_request` | diff --git a/cloudflare-pages-preview/cleanup/action.yml b/cloudflare-pages-preview/cleanup/action.yml index c01f822..ab0be58 100644 --- a/cloudflare-pages-preview/cleanup/action.yml +++ b/cloudflare-pages-preview/cleanup/action.yml @@ -22,6 +22,13 @@ inputs: description: HTML comment marker used to locate the sticky PR comment required: false default: '' + comment-mode: + description: >- + Must match the deploy action's comment-mode. 'sticky' (default) updates + the single marker comment in place. 'repost' minimizes all marker + comments as OUTDATED and posts a final cleanup comment. + required: false + default: sticky github-token: description: GitHub token for updating the PR comment required: false @@ -72,28 +79,10 @@ runs: uses: actions/github-script@v8 env: COMMENT_MARKER: ${{ inputs.comment-marker }} + COMMENT_MODE: ${{ inputs.comment-mode }} PR_NUMBER: ${{ inputs.pr-number }} with: github-token: ${{ inputs.github-token }} script: | - const { owner, repo } = context.repo; - const prNumber = Number(process.env.PR_NUMBER); - if (!prNumber) { - core.info('No PR number available — skipping comment update.'); - return; - } - const marker = process.env.COMMENT_MARKER; - - const { data: comments } = await github.rest.issues.listComments({ - owner, repo, issue_number: prNumber - }); - const existing = comments.find(c => - c.user.type === 'Bot' && c.body.includes(marker) - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, comment_id: existing.id, - body: `${marker}\n## Preview\n\n_Preview deployments for this PR have been cleaned up._` - }); - } + const main = require(process.env.GITHUB_ACTION_PATH + '/comment.js'); + await main({ github, context, core }); diff --git a/cloudflare-pages-preview/cleanup/comment.js b/cloudflare-pages-preview/cleanup/comment.js new file mode 100644 index 0000000..2514e01 --- /dev/null +++ b/cloudflare-pages-preview/cleanup/comment.js @@ -0,0 +1,51 @@ +// Updates the PR preview comment after deployments are deleted. Called by +// actions/github-script via `require(GITHUB_ACTION_PATH + '/comment.js')`. + +module.exports = async ({ github, context, core }) => { + const { owner, repo } = context.repo; + const prNumber = Number(process.env.PR_NUMBER); + if (!prNumber) { + core.info('No PR number available — skipping comment update.'); + return; + } + const marker = process.env.COMMENT_MARKER; + const mode = process.env.COMMENT_MODE; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: prNumber, per_page: 100, + }); + const prior = comments.filter(c => + c.user.type === 'Bot' && c.body && c.body.includes(marker) + ); + + const cleanupBody = `${marker}\n## Preview\n\n_Preview deployments for this PR have been cleaned up._`; + + if (mode === 'repost') { + for (const c of prior) { + try { + await github.graphql( + `mutation($id: ID!) { + minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { + minimizedComment { isMinimized } + } + }`, + { id: c.node_id }, + ); + } catch (e) { + core.warning(`Could not minimize comment ${c.id}: ${e.message}`); + } + } + if (prior.length) { + await github.rest.issues.createComment({ + owner, repo, issue_number: prNumber, body: cleanupBody, + }); + } + } else { + const existing = prior[0]; + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: existing.id, body: cleanupBody, + }); + } + } +}; diff --git a/cloudflare-pages-preview/deploy/action.yml b/cloudflare-pages-preview/deploy/action.yml index b331c62..a951861 100644 --- a/cloudflare-pages-preview/deploy/action.yml +++ b/cloudflare-pages-preview/deploy/action.yml @@ -36,6 +36,15 @@ inputs: description: Heading shown in the sticky PR comment required: false default: 'Preview Deployed' + comment-mode: + description: >- + How to handle prior preview comments. 'sticky' (default) updates the + existing marker comment in place. 'repost' minimizes all prior marker + comments as OUTDATED (GitHub's native hide-comment UI) and posts a fresh + comment at the bottom of the conversation — preserving history while + surfacing the latest links prominently. + required: false + default: sticky github-token: description: GitHub token for posting/updating the PR comment required: false @@ -106,52 +115,12 @@ runs: ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }} COMMENT_MARKER: ${{ inputs.comment-marker }} COMMENT_TITLE: ${{ inputs.comment-title }} + COMMENT_MODE: ${{ inputs.comment-mode }} INJECT_NOINDEX: ${{ inputs.inject-noindex }} PR_NUMBER: ${{ inputs.pr-number }} COMMIT_SHA: ${{ inputs.commit-sha }} with: github-token: ${{ inputs.github-token }} script: | - const { owner, repo } = context.repo; - const prNumber = Number(process.env.PR_NUMBER); - if (!prNumber) { - core.info('No PR number available — skipping comment.'); - return; - } - const deployUrl = process.env.DEPLOY_URL; - const aliasUrl = process.env.ALIAS_URL || deployUrl; - const marker = process.env.COMMENT_MARKER; - const title = process.env.COMMENT_TITLE; - const noindex = process.env.INJECT_NOINDEX === 'true'; - - const noindexNote = noindex - ? ' All pages served with `noindex, nofollow` — search engines will not crawl this preview.' - : ''; - - const body = `${marker} - ## ${title} - - | | | - |---|---| - | **Preview (stable)** | ${aliasUrl} | - | **This commit** | ${deployUrl} | - | **Commit** | \`${process.env.COMMIT_SHA.slice(0, 7)}\` | - - _Includes drafts and future-dated posts.${noindexNote}_`; - - const { data: comments } = await github.rest.issues.listComments({ - owner, repo, issue_number: prNumber - }); - const existing = comments.find(c => - c.user.type === 'Bot' && c.body.includes(marker) - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner, repo, comment_id: existing.id, body - }); - } else { - await github.rest.issues.createComment({ - owner, repo, issue_number: prNumber, body - }); - } + const main = require(process.env.GITHUB_ACTION_PATH + '/comment.js'); + await main({ github, context, core }); diff --git a/cloudflare-pages-preview/deploy/comment.js b/cloudflare-pages-preview/deploy/comment.js new file mode 100644 index 0000000..7fd2ace --- /dev/null +++ b/cloudflare-pages-preview/deploy/comment.js @@ -0,0 +1,76 @@ +// Posts or updates the PR preview-URL comment. Called by actions/github-script +// via `require(GITHUB_ACTION_PATH + '/comment.js')({ github, context, core })`. +// All inputs are passed via env vars (see action.yml). + +module.exports = async ({ github, context, core }) => { + const { owner, repo } = context.repo; + const prNumber = Number(process.env.PR_NUMBER); + if (!prNumber) { + core.info('No PR number available — skipping comment.'); + return; + } + const deployUrl = process.env.DEPLOY_URL; + const aliasUrl = process.env.ALIAS_URL || deployUrl; + const marker = process.env.COMMENT_MARKER; + const title = process.env.COMMENT_TITLE; + const mode = process.env.COMMENT_MODE; + const noindex = process.env.INJECT_NOINDEX === 'true'; + const commitSha = process.env.COMMIT_SHA; + + const noindexNote = noindex + ? ' All pages served with `noindex, nofollow` — search engines will not crawl this preview.' + : ''; + + const body = [ + marker, + `## ${title}`, + '', + '| | |', + '|---|---|', + `| **Preview (stable)** | ${aliasUrl} |`, + `| **This commit** | ${deployUrl} |`, + `| **Commit** | \`${commitSha.slice(0, 7)}\` |`, + '', + `_Includes drafts and future-dated posts.${noindexNote}_`, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: prNumber, per_page: 100, + }); + const prior = comments.filter(c => + c.user.type === 'Bot' && c.body && c.body.includes(marker) + ); + + if (mode === 'repost') { + for (const c of prior) { + try { + await github.graphql( + `mutation($id: ID!) { + minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) { + minimizedComment { isMinimized } + } + }`, + { id: c.node_id }, + ); + core.info(`Minimized prior comment ${c.id} as OUTDATED`); + } catch (e) { + core.warning(`Could not minimize comment ${c.id}: ${e.message}`); + } + } + await github.rest.issues.createComment({ + owner, repo, issue_number: prNumber, body, + }); + } else { + // sticky: update the first prior comment in place, or create one + const existing = prior[0]; + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: existing.id, body, + }); + } else { + await github.rest.issues.createComment({ + owner, repo, issue_number: prNumber, body, + }); + } + } +};