Skip to content

Commit 30157a8

Browse files
steve-downeyclaude
andcommitted
docs: split PR preview comment into a separate workflow_run job
Fork PRs use the normal beman workflow. The GITHUB_TOKEN in a pull_request run from a fork is always read-only, but the workflow_run event fires code from the BASE branch with write permissions. This is GitHub's recommended pattern for trusted actions on untrusted PRs. - docs.yml: remove pull-requests: write (no longer needed); remove the inline comment step entirely. - docs-comment.yml: new workflow triggered by workflow_run on "Documentation" completion. Posts/updates the preview comment using a write-capable token. Finds the PR by searching for an open PR whose head matches the triggering run's head_repository + head_branch, so it works for both same-repo and fork PRs. No fork code executes in docs-comment.yml; only trusted metadata from the workflow_run context is used. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent ccc1cd0 commit 30157a8

2 files changed

Lines changed: 93 additions & 47 deletions

File tree

.github/workflows/docs-comment.yml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
2+
3+
# Posts a PR preview comment after the Documentation workflow completes.
4+
#
5+
# This is intentionally a separate workflow from docs.yml. The
6+
# `pull_request` event (used in docs.yml) always runs with a read-only
7+
# GITHUB_TOKEN for fork PRs, so it cannot post comments. The
8+
# `workflow_run` event runs code from the BASE branch — never from the
9+
# fork — and is granted write permissions safely. No fork code executes
10+
# here; we only read trusted metadata from the workflow_run context.
11+
#
12+
# Reference: https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run
13+
14+
name: Documentation Preview Comment
15+
16+
on:
17+
workflow_run:
18+
workflows: ["Documentation"]
19+
types: [completed]
20+
21+
permissions:
22+
pull-requests: write
23+
24+
jobs:
25+
comment:
26+
name: Post preview link
27+
runs-on: ubuntu-latest
28+
# Only comment on PR builds that succeeded. Push and
29+
# workflow_dispatch builds don't have a PR to comment on.
30+
if: >
31+
github.event.workflow_run.event == 'pull_request' &&
32+
github.event.workflow_run.conclusion == 'success'
33+
34+
steps:
35+
- name: Harden the runner (Audit all outbound calls)
36+
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
37+
with:
38+
egress-policy: audit
39+
40+
- name: Post or update PR preview comment
41+
uses: actions/github-script@v7
42+
with:
43+
script: |
44+
const MARKER = '<!-- docs-preview-comment -->'
45+
const run = context.payload.workflow_run
46+
const runUrl = run.html_url
47+
const sha = run.head_sha.slice(0, 7)
48+
const body = [
49+
MARKER,
50+
`📚 **Documentation preview** for \`${sha}\` — [workflow run](${runUrl})`,
51+
'',
52+
'To review: open the **docs-site** artifact from that run,',
53+
'extract the zip, and open `index.html` in a browser.',
54+
].join('\n')
55+
56+
// Locate the open PR that matches this workflow run's head
57+
// branch. For same-repo PRs github.event.pull_request is
58+
// available directly; for fork PRs we search by head label.
59+
const headLabel = `${run.head_repository.owner.login}:${run.head_branch}`
60+
const { data: prs } = await github.rest.pulls.list({
61+
owner: context.repo.owner,
62+
repo: context.repo.repo,
63+
head: headLabel,
64+
state: 'open',
65+
})
66+
if (prs.length === 0) {
67+
core.info(`No open PR found for head ${headLabel}; skipping comment.`)
68+
return
69+
}
70+
const issue_number = prs[0].number
71+
72+
const { data: comments } = await github.rest.issues.listComments({
73+
owner: context.repo.owner,
74+
repo: context.repo.repo,
75+
issue_number,
76+
})
77+
const existing = comments.find(c => c.body.includes(MARKER))
78+
if (existing) {
79+
await github.rest.issues.updateComment({
80+
owner: context.repo.owner,
81+
repo: context.repo.repo,
82+
comment_id: existing.id,
83+
body,
84+
})
85+
} else {
86+
await github.rest.issues.createComment({
87+
owner: context.repo.owner,
88+
repo: context.repo.repo,
89+
issue_number,
90+
body,
91+
})
92+
}

.github/workflows/docs.yml

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ jobs:
1515
name: Build
1616
runs-on: ubuntu-latest
1717
permissions:
18-
contents: write # needed for gh-pages deploy on main
19-
pull-requests: write # needed to post the preview comment on PRs
18+
contents: write # needed for gh-pages deploy on main
2019

2120
steps:
2221
- name: Harden the runner (Audit all outbound calls)
@@ -58,51 +57,6 @@ jobs:
5857
path: ${{ steps.out.outputs.dir }}/
5958
retention-days: 14
6059

61-
# Posts a comment on the PR with a direct link to the workflow run
62-
# where the docs-site artifact can be downloaded and inspected.
63-
# Uses a hidden marker so the comment is updated (not duplicated) on
64-
# each push to the PR branch.
65-
# Skip on fork PRs: the GITHUB_TOKEN in a fork PR run is always
66-
# read-only regardless of the permissions: block, so the comment
67-
# would 403. Only attempt it when the PR is within the same repo.
68-
- name: Post or update PR preview comment
69-
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
70-
uses: actions/github-script@v7
71-
with:
72-
script: |
73-
const MARKER = '<!-- docs-preview-comment -->'
74-
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
75-
const sha = context.payload.pull_request.head.sha.slice(0, 7)
76-
const body = [
77-
MARKER,
78-
`📚 **Documentation preview** for \`${sha}\` — [workflow run](${runUrl})`,
79-
'',
80-
'To review: open the **docs-site** artifact from that run,',
81-
'extract the zip, and open `index.html` in a browser.',
82-
].join('\n')
83-
84-
const { data: comments } = await github.rest.issues.listComments({
85-
owner: context.repo.owner,
86-
repo: context.repo.repo,
87-
issue_number: context.issue.number,
88-
})
89-
const existing = comments.find(c => c.body.includes(MARKER))
90-
if (existing) {
91-
await github.rest.issues.updateComment({
92-
owner: context.repo.owner,
93-
repo: context.repo.repo,
94-
comment_id: existing.id,
95-
body,
96-
})
97-
} else {
98-
await github.rest.issues.createComment({
99-
owner: context.repo.owner,
100-
repo: context.repo.repo,
101-
issue_number: context.issue.number,
102-
body,
103-
})
104-
}
105-
10660
- name: Deploy to GitHub Pages
10761
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
10862
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0

0 commit comments

Comments
 (0)