Skip to content

Commit 2526837

Browse files
fix: handle fork PR permission issue for pr-comments gracefully (#215)
* feat: implement graceful fork PR degradation and document workflow_run pattern - Add Job Summary notice when PR comments are skipped for fork PRs (not just a ::warning:: log line, but also visible in GH UI) - Update warning message to reference new documentation section - Add comprehensive Fork PR Comments section to README with: - Two-workflow (workflow_run) pattern as recommended best practice - pull_request_target alternative with security warnings - Add unit tests for the new Job Summary behavior Closes #143 * docs: move workflow_run examples to examples/ dir * fix: do not skip fork PR comments on pull_request_target * fix: get PR number from event payload on pull_request_target * chore: auto fixes from pre-commit.com hooks * docs: move fork PR docs to docs/fork-pr-comments.md for stable linking Code now links to docs/fork-pr-comments.md (a fixed file path) instead of README section anchors (#fork-pr-comments). This prevents link rot when the README gets restructured. README keeps a brief summary with a link to the dedicated docs file. * chore: remove accidentally committed result.txt, add to gitignore --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c7245f6 commit 2526837

7 files changed

Lines changed: 426 additions & 13 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
venv/
22
.venv/
33
__pycache__/
4+
result.txt

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ A GitHub Action for checking commit message formatting, branch naming, committer
2626
* [Optional Inputs](#optional-inputs)
2727
* [GitHub Action Job Summary](#github-action-job-summary)
2828
* [GitHub Pull Request Comments](#github-pull-request-comments)
29+
* [Fork PR Comments](docs/fork-pr-comments.md)
2930
* [Badging Your Repository](#badging-your-repository)
3031
* [Versioning](#versioning)
3132

@@ -123,7 +124,12 @@ jobs:
123124
> [!IMPORTANT]
124125
> `pr-comments` is an experimental feature. By default, it's disabled.
125126
>
126-
> PR comments are skipped for pull requests from forked repositories. For more details, refer to issue [`#143`](https://github.com/commit-check/commit-check-action/issues/143).
127+
> PR comments are skipped for pull requests from forked repositories. See
128+
> [docs/fork-pr-comments.md](docs/fork-pr-comments.md) for details on how to enable
129+
> this feature for fork contributions.
130+
>
131+
> Note: write-access to pull-requests requires the `pull-requests: write` permission.
132+
> See [usage example](#usage).
127133

128134
Note: the default rule of above inputs is following [this configuration](https://github.com/commit-check/commit-check-action/blob/main/commit-check.toml). If you want to customize, just add your [`commit-check.toml`](https://commit-check.github.io/commit-check/configuration.html) config file under your repository root directory.
129135

@@ -149,6 +155,23 @@ By default, commit-check-action results are shown on the job summary page of the
149155

150156
![Failure pull request comment](https://github.com/commit-check/.github/blob/main/screenshot/failure-pr-comments.png)
151157

158+
## Fork PR Comments
159+
160+
When a pull request is opened from a **forked repository**, the `GITHUB_TOKEN` used by the
161+
`pull_request` event has **read-only** permissions by design (GitHub security policy).
162+
This means `pr-comments: true` cannot write a comment back to the PR.
163+
164+
By default, commit-check-action handles this gracefully:
165+
166+
- PR comment writing is **skipped** with a `::warning::` message in the logs
167+
- A **notice is added to the Job Summary** explaining why and how to fix it
168+
- The commit checks themselves **still run normally**
169+
170+
> **For most projects, this is sufficient** — contributors can see check results in the
171+
> action Job Summary. But if you *must* have PR comments on fork contributions, see
172+
> the **[Fork PR Comments](docs/fork-pr-comments.md)** documentation for
173+
> two recommended approaches with ready-to-use workflow examples.
174+
152175
## Badging Your Repository
153176

154177
You can add a badge to your repository to show your contributors/users that you use commit-check!

docs/fork-pr-comments.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Fork PR Comments
2+
3+
When a pull request is opened from a **forked repository**, the `GITHUB_TOKEN` used by the
4+
`pull_request` event has **read-only** permissions by design (GitHub security policy).
5+
This means `pr-comments: true` cannot write a comment back to the PR.
6+
7+
By default, commit-check-action handles this gracefully:
8+
9+
- PR comment writing is **skipped** with a `::warning::` message in the logs
10+
- A **notice is added to the Job Summary** explaining why and how to fix it
11+
- The commit checks themselves **still run normally**
12+
13+
> **For most projects, this is sufficient** — contributors can see check results in the
14+
> action Job Summary. But if you *must* have PR comments on fork contributions, there
15+
> are two recommended approaches.
16+
17+
---
18+
19+
## Option 1: Two-workflow pattern (recommended)
20+
21+
This is the **official GitHub-recommended best practice** for writing PR comments from
22+
fork PRs. It uses the [`workflow_run`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run)
23+
event with **no security risks**.
24+
25+
> 📁 Ready-to-use files: [`examples/commit-check-workflow-a.yml`](../examples/commit-check-workflow-a.yml)
26+
> and [`examples/commit-check-workflow-b.yml`](../examples/commit-check-workflow-b.yml)
27+
28+
**How it works:**
29+
30+
```
31+
pull_request workflow_run
32+
│ │
33+
▼ ▼
34+
┌──────────────┐ ┌──────────────────┐
35+
│ Workflow A │ │ Workflow B │
36+
│ (checks) │────►│ (comment writer) │
37+
│ │ │ │
38+
│ Token: READ │ │ Token: WRITE │
39+
│ Saves result │ │ Reads artifact │
40+
│ as artifact │ │ Posts PR comment │
41+
└──────────────┘ └──────────────────┘
42+
```
43+
44+
### Workflow A
45+
46+
`.github/workflows/commit-check.yml` (triggered by `pull_request`):
47+
48+
```yaml
49+
name: Commit Check
50+
51+
on:
52+
pull_request:
53+
branches: ["main"]
54+
55+
jobs:
56+
check:
57+
runs-on: ubuntu-latest
58+
steps:
59+
- uses: actions/checkout@v5
60+
with:
61+
fetch-depth: 0
62+
- uses: commit-check/commit-check-action@v2
63+
with:
64+
message: true
65+
branch: true
66+
pr-comments: false # comments handled by Workflow B
67+
job-summary: true
68+
- uses: actions/upload-artifact@v4
69+
with:
70+
name: commit-check-result-${{ github.event.number }}
71+
path: result.txt # saved for Workflow B
72+
```
73+
74+
> 📄 Full file: [`examples/commit-check-workflow-a.yml`](../examples/commit-check-workflow-a.yml)
75+
76+
### Workflow B
77+
78+
`.github/workflows/commit-check-comment.yml` (triggered by `workflow_run`):
79+
80+
```yaml
81+
name: Commit Check Comment
82+
83+
on:
84+
workflow_run:
85+
workflows: ["Commit Check"] # must match Workflow A's name exactly
86+
types: [completed]
87+
88+
jobs:
89+
comment:
90+
runs-on: ubuntu-latest
91+
permissions:
92+
pull-requests: write
93+
actions: read # needed to download artifacts
94+
steps:
95+
- uses: actions/download-artifact@v4
96+
with:
97+
name: commit-check-result-${{ github.event.workflow_run.pull_requests[0].number }}
98+
run-id: ${{ github.event.workflow_run.id }}
99+
github-token: ${{ github.token }}
100+
- name: Read result and post PR comment
101+
uses: actions/github-script@v7
102+
with:
103+
script: |
104+
// See examples/commit-check-workflow-b.yml for full script
105+
const fs = require('fs');
106+
const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }};
107+
const resultText = fs.readFileSync('result.txt', 'utf8').trim();
108+
const body = resultText
109+
? '# Commit-Check ❌\n```\n' + resultText + '\n```'
110+
: '# Commit-Check ✔️';
111+
// Creates or updates the matching PR comment
112+
```
113+
114+
> 📄 Full file: [`examples/commit-check-workflow-b.yml`](../examples/commit-check-workflow-b.yml)
115+
116+
### Key security benefits
117+
118+
- Workflow B runs in the **base repository's context**, so `GITHUB_TOKEN` has full write
119+
permissions (you explicitly grant `pull-requests: write`)
120+
- Workflow B **does not checkout the PR code**, so untrusted fork code never runs
121+
with elevated permissions
122+
- The artifact only contains `result.txt` — no code or secrets
123+
124+
---
125+
126+
## Option 2: pull_request_target (advanced, use with caution)
127+
128+
If you understand the security implications, you can use
129+
[`pull_request_target`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target)
130+
which runs in the base repository's context with **write token access**.
131+
132+
> **⚠️ Security warning:** Never check out (`actions/checkout`) the PR's HEAD commit
133+
> when using `pull_request_target`. Always check out the base branch or use the
134+
> default merge commit. Otherwise, fork code could exfiltrate your repository's secrets.
135+
136+
```yaml
137+
name: Commit Check
138+
139+
on:
140+
pull_request_target:
141+
branches: ["main"]
142+
143+
jobs:
144+
commit-check:
145+
runs-on: ubuntu-latest
146+
permissions:
147+
contents: read
148+
pull-requests: write
149+
steps:
150+
# SAFE: checkout the merge commit, NOT the PR head
151+
- uses: actions/checkout@v5
152+
with:
153+
fetch-depth: 0
154+
- uses: commit-check/commit-check-action@v2
155+
with:
156+
message: true
157+
branch: true
158+
pr-comments: true
159+
```
160+
161+
> ✅ With `pull_request_target`, `pr-comments: true` **does work** on fork PRs —
162+
> the token has the workflow's configured permissions regardless of whether the PR
163+
> is from a fork.
164+
>
165+
> **When to use this:** Only if the two-workflow pattern is too complex for your setup
166+
> and you have thoroughly reviewed the security implications.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Workflow A: Run commit checks on pull_request events.
2+
#
3+
# This workflow is triggered by pull_request and runs commit checks.
4+
# It uploads the result as an artifact so Workflow B (commit-check-comment.yml)
5+
# can read it and post a PR comment with full write permissions.
6+
#
7+
# See https://github.com/commit-check/commit-check-action#fork-pr-comments
8+
9+
name: Commit Check
10+
11+
on:
12+
pull_request:
13+
branches: ["main"]
14+
15+
jobs:
16+
check:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v5
20+
with:
21+
fetch-depth: 0
22+
- uses: commit-check/commit-check-action@v2
23+
with:
24+
message: true
25+
branch: true
26+
pr-comments: false # comments handled by Workflow B
27+
job-summary: true
28+
29+
# Save results so Workflow B can post a PR comment
30+
- uses: actions/upload-artifact@v4
31+
with:
32+
name: commit-check-result-${{ github.event.number }}
33+
path: result.txt
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Workflow B: Post PR comment after commit checks complete.
2+
#
3+
# This workflow is triggered by the workflow_run event from Workflow A.
4+
# It runs in the base repository's context with full write permissions,
5+
# making it safe for fork PRs (no checkout of fork code).
6+
#
7+
# Prerequisites:
8+
# - Workflow A (commit-check.yml) must exist and upload an artifact named
9+
# commit-check-result-<PR-number> containing result.txt
10+
#
11+
# See https://github.com/commit-check/commit-check-action#fork-pr-comments
12+
13+
name: Commit Check Comment
14+
15+
on:
16+
workflow_run:
17+
workflows: ["Commit Check"] # must match Workflow A's name exactly
18+
types: [completed]
19+
20+
jobs:
21+
comment:
22+
runs-on: ubuntu-latest
23+
permissions:
24+
pull-requests: write
25+
actions: read # needed to download artifacts
26+
steps:
27+
- uses: actions/download-artifact@v4
28+
with:
29+
name: commit-check-result-${{ github.event.workflow_run.pull_requests[0].number }}
30+
run-id: ${{ github.event.workflow_run.id }}
31+
github-token: ${{ github.token }}
32+
33+
- name: Read result and post PR comment
34+
uses: actions/github-script@v7
35+
with:
36+
script: |
37+
const fs = require('fs');
38+
const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }};
39+
const resultText = fs.readFileSync('result.txt', 'utf8').trim();
40+
41+
const successTitle = '# Commit-Check ✔️';
42+
const failureTitle = '# Commit-Check ❌';
43+
const body = resultText
44+
? `${failureTitle}\n\`\`\`\n${resultText}\n\`\`\``
45+
: successTitle;
46+
47+
const { data: comments } = await github.rest.issues.listComments({
48+
...context.repo,
49+
issue_number: prNumber,
50+
});
51+
52+
const existing = comments.find(c =>
53+
c.body.startsWith(successTitle) || c.body.startsWith(failureTitle)
54+
);
55+
56+
if (existing) {
57+
await github.rest.issues.updateComment({
58+
...context.repo,
59+
comment_id: existing.id,
60+
body,
61+
});
62+
} else {
63+
await github.rest.issues.createComment({
64+
...context.repo,
65+
issue_number: prNumber,
66+
body,
67+
});
68+
}

0 commit comments

Comments
 (0)