cloudflare-pages-preview: add comment-mode=repost, extract scripts

New comment-mode input:
- 'sticky' (default, backwards-compat): update the existing marker comment
  in place — same behavior as before
- 'repost': minimize all prior marker comments as OUTDATED (GitHub's native
  hide-comment UI via GraphQL minimizeComment mutation) and post a fresh
  comment at the bottom of the conversation

Useful for /stageblog where each staging should produce a visible new
comment while keeping history collapsible rather than lost.

Both deploy and cleanup accept the input and must match. cleanup in repost
mode minimizes all prior comments and posts a final cleanup note.

Also:
- Extract both comment scripts to sibling comment.js files (same pattern as
  slash-commands/main.js) — the inline YAML scripts were getting long
- Paginate listComments (was only fetching the first 30)
This commit is contained in:
Den Delimarsky 2026-03-11 23:24:51 +00:00
commit e6b64f4192
No known key found for this signature in database
5 changed files with 154 additions and 65 deletions

View file

@ -11,7 +11,9 @@ and cleaning them up when the PR closes.
previews aren't crawled by search engines (optional, on by default) previews aren't crawled by search engines (optional, on by default)
2. Deploys to Cloudflare Pages under a per-PR branch alias 2. Deploys to Cloudflare Pages under a per-PR branch alias
(`pr-<number>` by default) giving a stable URL across force-pushes (`pr-<number>` 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: - **`cleanup`** — On PR close:
1. Deletes all Cloudflare Pages deployments for the PR's branch alias 1. Deletes all Cloudflare Pages deployments for the PR's branch alias
@ -137,6 +139,7 @@ Cloudflare API and GitHub API.
| `branch` | | `pr-<PR number>` | CF Pages branch alias | | `branch` | | `pr-<PR number>` | CF Pages branch alias |
| `inject-noindex` | | `'true'` | Inject `_headers` + `<meta>` noindex | | `inject-noindex` | | `'true'` | Inject `_headers` + `<meta>` noindex |
| `comment-marker` | | `<!-- cf-pages-preview -->` | Sticky-comment identifier | | `comment-marker` | | `<!-- cf-pages-preview -->` | 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 | | `comment-title` | | `Preview Deployed` | Heading in PR comment |
| `github-token` | | `${{ github.token }}` | Token for PR commenting | | `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` | | `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 | | `account-id` | ✅ | — | Cloudflare account ID |
| `branch` | | `pr-<PR number>` | CF Pages branch alias to delete | | `branch` | | `pr-<PR number>` | CF Pages branch alias to delete |
| `comment-marker` | | `<!-- cf-pages-preview -->` | Sticky-comment identifier | | `comment-marker` | | `<!-- cf-pages-preview -->` | 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 | | `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` | | `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` |

View file

@ -22,6 +22,13 @@ inputs:
description: HTML comment marker used to locate the sticky PR comment description: HTML comment marker used to locate the sticky PR comment
required: false required: false
default: '<!-- cf-pages-preview -->' default: '<!-- cf-pages-preview -->'
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: github-token:
description: GitHub token for updating the PR comment description: GitHub token for updating the PR comment
required: false required: false
@ -72,28 +79,10 @@ runs:
uses: actions/github-script@v8 uses: actions/github-script@v8
env: env:
COMMENT_MARKER: ${{ inputs.comment-marker }} COMMENT_MARKER: ${{ inputs.comment-marker }}
COMMENT_MODE: ${{ inputs.comment-mode }}
PR_NUMBER: ${{ inputs.pr-number }} PR_NUMBER: ${{ inputs.pr-number }}
with: with:
github-token: ${{ inputs.github-token }} github-token: ${{ inputs.github-token }}
script: | script: |
const { owner, repo } = context.repo; const main = require(process.env.GITHUB_ACTION_PATH + '/comment.js');
const prNumber = Number(process.env.PR_NUMBER); await main({ github, context, core });
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._`
});
}

View file

@ -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,
});
}
}
};

View file

@ -36,6 +36,15 @@ inputs:
description: Heading shown in the sticky PR comment description: Heading shown in the sticky PR comment
required: false required: false
default: 'Preview Deployed' 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: github-token:
description: GitHub token for posting/updating the PR comment description: GitHub token for posting/updating the PR comment
required: false required: false
@ -106,52 +115,12 @@ runs:
ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }} ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
COMMENT_MARKER: ${{ inputs.comment-marker }} COMMENT_MARKER: ${{ inputs.comment-marker }}
COMMENT_TITLE: ${{ inputs.comment-title }} COMMENT_TITLE: ${{ inputs.comment-title }}
COMMENT_MODE: ${{ inputs.comment-mode }}
INJECT_NOINDEX: ${{ inputs.inject-noindex }} INJECT_NOINDEX: ${{ inputs.inject-noindex }}
PR_NUMBER: ${{ inputs.pr-number }} PR_NUMBER: ${{ inputs.pr-number }}
COMMIT_SHA: ${{ inputs.commit-sha }} COMMIT_SHA: ${{ inputs.commit-sha }}
with: with:
github-token: ${{ inputs.github-token }} github-token: ${{ inputs.github-token }}
script: | script: |
const { owner, repo } = context.repo; const main = require(process.env.GITHUB_ACTION_PATH + '/comment.js');
const prNumber = Number(process.env.PR_NUMBER); await main({ github, context, core });
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
});
}

View file

@ -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,
});
}
}
};