Skip to content

Commit fc263cd

Browse files
ci: add PR bump preview workflow (#1957)
* ci: add PR bump preview workflow Adds a workflow that runs cz bump --dry-run on incoming pull requests and posts (or updates) a sticky comment summarising the would-be version bump and changelog entries. This makes unexpected version bumps visible to reviewers before merging, addressing #1510. The pattern is documented in docs/tutorials/github_actions.md so other projects can copy/paste the same workflow. Closes #1510 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ci): gate PR bump preview to same-repo PRs only Address Copilot review feedback on #1957: * `cz bump` renders Jinja templates from the working directory whenever `update_changelog_on_bump` is set in config, using a non-sandboxed `FileSystemLoader('.')`. Under `pull_request_target` with a write token, executing those templates against fork-controlled files would risk RCE / token exfiltration. Gate the job to same-repo PRs by comparing `head.repo.full_name` to `base.repo.full_name`. * Set `persist-credentials: false` on `actions/checkout` as defense in depth, so the workflow token is not written to `.git/config`. * Adjust docs to drop the misleading `and changelog entries` claim (the dry-run only shows changelog entries when `update_changelog_on_bump` is enabled), and rewrite the safety explanation to reflect the real threat model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 40aa19d commit fc263cd

2 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: PR bump preview
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened, synchronize, ready_for_review]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: write
10+
11+
jobs:
12+
bump-preview:
13+
# Skip drafts, and skip fork PRs entirely. `pull_request_target` runs with
14+
# the base repo's GITHUB_TOKEN (write access to PR comments). `cz bump`
15+
# can render Jinja templates from the checked-out workspace whenever
16+
# `update_changelog_on_bump` is set in config, and the renderer is not
17+
# sandboxed (FileSystemLoader('.')) — running it against fork-controlled
18+
# files would risk RCE / token exfiltration. Same-repo PRs are written by
19+
# collaborators who already have push access, so the same risk doesn't
20+
# apply.
21+
if: >
22+
${{
23+
github.event.pull_request.draft == false &&
24+
github.event.pull_request.head.repo.full_name ==
25+
github.event.pull_request.base.repo.full_name
26+
}}
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Check out PR head
30+
uses: actions/checkout@v6
31+
with:
32+
ref: ${{ github.event.pull_request.head.sha }}
33+
fetch-depth: 0
34+
fetch-tags: true
35+
# Defense in depth: don't write the workflow token to .git/config.
36+
persist-credentials: false
37+
38+
- name: Set up Commitizen
39+
uses: commitizen-tools/setup-cz@main
40+
with:
41+
set-git-config: false
42+
43+
- name: Run cz bump --dry-run
44+
id: dry-run
45+
run: |
46+
set +e
47+
output="$(cz bump --dry-run --yes 2>&1)"
48+
status=$?
49+
set -e
50+
{
51+
echo "status=${status}"
52+
echo "output<<__CZ_BUMP_PREVIEW__"
53+
printf '%s\n' "${output}"
54+
echo "__CZ_BUMP_PREVIEW__"
55+
} >> "$GITHUB_OUTPUT"
56+
57+
- name: Build comment body
58+
env:
59+
STATUS: ${{ steps.dry-run.outputs.status }}
60+
OUTPUT: ${{ steps.dry-run.outputs.output }}
61+
run: |
62+
{
63+
echo "<!-- commitizen-bump-preview -->"
64+
echo "## 🔍 Commitizen bump preview"
65+
echo ""
66+
case "${STATUS}" in
67+
0)
68+
echo "Merging this PR will produce the following bump:"
69+
echo ""
70+
echo '```'
71+
printf '%s\n' "${OUTPUT}"
72+
echo '```'
73+
;;
74+
21)
75+
echo "No commits in this PR are eligible for a version bump."
76+
;;
77+
*)
78+
echo "⚠️ \`cz bump --dry-run\` exited with status \`${STATUS}\`:"
79+
echo ""
80+
echo '```'
81+
printf '%s\n' "${OUTPUT}"
82+
echo '```'
83+
;;
84+
esac
85+
} > comment.md
86+
87+
- name: Post or update PR comment
88+
uses: peter-evans/create-or-update-comment@v4
89+
with:
90+
token: ${{ secrets.GITHUB_TOKEN }}
91+
issue-number: ${{ github.event.pull_request.number }}
92+
body-path: comment.md
93+
body-includes: "<!-- commitizen-bump-preview -->"
94+
edit-mode: replace

docs/tutorials/github_actions.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,127 @@ jobs:
123123

124124
You can find the complete workflow in our repository at [bumpversion.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/bumpversion.yml).
125125

126+
### Previewing the version bump on pull requests
127+
128+
To help reviewers spot unexpected version bumps before merging, you can run
129+
`cz bump --dry-run` on every pull request and post (or update) a sticky
130+
comment summarizing the would-be version bump.
131+
132+
Create `.github/workflows/pr-bump-preview.yml`:
133+
134+
```yaml title=".github/workflows/pr-bump-preview.yml"
135+
name: PR bump preview
136+
137+
on:
138+
pull_request_target:
139+
types: [opened, reopened, synchronize, ready_for_review]
140+
141+
permissions:
142+
contents: read
143+
pull-requests: write
144+
145+
jobs:
146+
bump-preview:
147+
# Skip drafts and fork PRs (see "How it works" below).
148+
if: >
149+
${{
150+
github.event.pull_request.draft == false &&
151+
github.event.pull_request.head.repo.full_name ==
152+
github.event.pull_request.base.repo.full_name
153+
}}
154+
runs-on: ubuntu-latest
155+
steps:
156+
- name: Check out PR head
157+
uses: actions/checkout@v6
158+
with:
159+
ref: ${{ github.event.pull_request.head.sha }}
160+
fetch-depth: 0
161+
fetch-tags: true
162+
persist-credentials: false
163+
- uses: commitizen-tools/setup-cz@main
164+
with:
165+
set-git-config: false
166+
- name: Run cz bump --dry-run
167+
id: dry-run
168+
run: |
169+
set +e
170+
output="$(cz bump --dry-run --yes 2>&1)"
171+
status=$?
172+
set -e
173+
{
174+
echo "status=${status}"
175+
echo "output<<__CZ_BUMP_PREVIEW__"
176+
printf '%s\n' "${output}"
177+
echo "__CZ_BUMP_PREVIEW__"
178+
} >> "$GITHUB_OUTPUT"
179+
- name: Build comment body
180+
env:
181+
STATUS: ${{ steps.dry-run.outputs.status }}
182+
OUTPUT: ${{ steps.dry-run.outputs.output }}
183+
run: |
184+
{
185+
echo "<!-- commitizen-bump-preview -->"
186+
echo "## 🔍 Commitizen bump preview"
187+
echo ""
188+
case "${STATUS}" in
189+
0)
190+
echo "Merging this PR will produce the following bump:"
191+
echo ""
192+
echo '```'
193+
printf '%s\n' "${OUTPUT}"
194+
echo '```'
195+
;;
196+
21)
197+
echo "No commits in this PR are eligible for a version bump."
198+
;;
199+
*)
200+
echo "⚠️ \`cz bump --dry-run\` exited with status \`${STATUS}\`:"
201+
echo ""
202+
echo '```'
203+
printf '%s\n' "${OUTPUT}"
204+
echo '```'
205+
;;
206+
esac
207+
} > comment.md
208+
- uses: peter-evans/create-or-update-comment@v4
209+
with:
210+
token: ${{ secrets.GITHUB_TOKEN }}
211+
issue-number: ${{ github.event.pull_request.number }}
212+
body-path: comment.md
213+
body-includes: "<!-- commitizen-bump-preview -->"
214+
edit-mode: replace
215+
```
216+
217+
#### How it works
218+
219+
- **Trigger**: `pull_request_target` runs in the context of the base
220+
repository, which gives the workflow `pull-requests: write` permission
221+
even for PRs from forks. We deliberately gate the job to **same-repo PRs
222+
only** (`head.repo == base.repo`); fork PRs are skipped. This is because
223+
`cz bump` renders [Jinja templates from the working directory][jinja]
224+
whenever [`update_changelog_on_bump`](../config/configuration_file.md) is
225+
enabled, and the renderer is not sandboxed — running it against
226+
fork-controlled files under a write token would risk arbitrary code
227+
execution and token exfiltration. Same-repo PRs are written by
228+
collaborators who already have push access, so the same risk doesn't
229+
apply.
230+
- **Setup**: [`commitizen-tools/setup-cz`](https://github.com/commitizen-tools/setup-cz)
231+
installs the Commitizen CLI; no language-specific build tooling is required.
232+
- **Defense in depth**: `persist-credentials: false` on `actions/checkout`
233+
keeps the workflow token out of the local git config.
234+
- **Dry-run**: `cz bump --dry-run --yes` computes the next version (and, if
235+
`update_changelog_on_bump` is set in your config, also the changelog
236+
entries that would be produced). Exit code `21` (`NoneIncrementExit`)
237+
is treated as "no eligible bump" rather than a failure.
238+
- **Sticky comment**: The hidden HTML marker `<!-- commitizen-bump-preview -->`
239+
lets [`peter-evans/create-or-update-comment`](https://github.com/peter-evans/create-or-update-comment)
240+
find and replace the previous preview on every push, instead of leaving a
241+
growing trail of comments.
242+
243+
[jinja]: https://github.com/commitizen-tools/commitizen/blob/master/commitizen/changelog.py
244+
245+
You can find the complete workflow in our repository at [pr-bump-preview.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/pr-bump-preview.yml).
246+
126247
### Publishing a Python package
127248

128249
After a new version tag is created by the bump workflow, you can automatically publish your package to PyPI.

0 commit comments

Comments
 (0)