From 3af063c38047d1eb8c8f7ab9007a576a4f7419b0 Mon Sep 17 00:00:00 2001 From: Tim Hsiung Date: Sat, 9 May 2026 15:12:08 +0800 Subject: [PATCH 1/2] feat(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> --- .github/workflows/pr-bump-preview.yml | 83 +++++++++++++++++++++ docs/tutorials/github_actions.md | 102 ++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 .github/workflows/pr-bump-preview.yml diff --git a/.github/workflows/pr-bump-preview.yml b/.github/workflows/pr-bump-preview.yml new file mode 100644 index 000000000..b85123f34 --- /dev/null +++ b/.github/workflows/pr-bump-preview.yml @@ -0,0 +1,83 @@ +name: PR bump preview + +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: read + pull-requests: write + +jobs: + bump-preview: + if: ${{ github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + steps: + # Using pull_request_target means GITHUB_TOKEN has write access to PR + # comments even for fork PRs. We deliberately only run `cz bump --dry-run` + # against the checked-out PR commits — no PR-controlled scripts are + # executed, so this is safe. + - name: Check out PR head + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + fetch-tags: true + + - name: Set up Commitizen + uses: commitizen-tools/setup-cz@main + with: + set-git-config: false + + - name: Run cz bump --dry-run + id: dry-run + run: | + set +e + output="$(cz bump --dry-run --yes 2>&1)" + status=$? + set -e + { + echo "status=${status}" + echo "output<<__CZ_BUMP_PREVIEW__" + printf '%s\n' "${output}" + echo "__CZ_BUMP_PREVIEW__" + } >> "$GITHUB_OUTPUT" + + - name: Build comment body + env: + STATUS: ${{ steps.dry-run.outputs.status }} + OUTPUT: ${{ steps.dry-run.outputs.output }} + run: | + { + echo "" + echo "## 🔍 Commitizen bump preview" + echo "" + case "${STATUS}" in + 0) + echo "Merging this PR will produce the following bump:" + echo "" + echo '```' + printf '%s\n' "${OUTPUT}" + echo '```' + ;; + 21) + echo "No commits in this PR are eligible for a version bump." + ;; + *) + echo "⚠️ \`cz bump --dry-run\` exited with status \`${STATUS}\`:" + echo "" + echo '```' + printf '%s\n' "${OUTPUT}" + echo '```' + ;; + esac + } > comment.md + + - name: Post or update PR comment + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body-path: comment.md + body-includes: "" + edit-mode: replace diff --git a/docs/tutorials/github_actions.md b/docs/tutorials/github_actions.md index 24a55fc79..46b196b8d 100644 --- a/docs/tutorials/github_actions.md +++ b/docs/tutorials/github_actions.md @@ -123,6 +123,108 @@ jobs: You can find the complete workflow in our repository at [bumpversion.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/bumpversion.yml). +### Previewing the version bump on pull requests + +To help reviewers spot unexpected version bumps before merging, you can run +`cz bump --dry-run` on every pull request and post (or update) a sticky +comment summarizing the would-be version bump and changelog entries. + +Create `.github/workflows/pr-bump-preview.yml`: + +```yaml title=".github/workflows/pr-bump-preview.yml" +name: PR bump preview + +on: + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: read + pull-requests: write + +jobs: + bump-preview: + if: ${{ github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + steps: + - name: Check out PR head + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + fetch-tags: true + - uses: commitizen-tools/setup-cz@main + with: + set-git-config: false + - name: Run cz bump --dry-run + id: dry-run + run: | + set +e + output="$(cz bump --dry-run --yes 2>&1)" + status=$? + set -e + { + echo "status=${status}" + echo "output<<__CZ_BUMP_PREVIEW__" + printf '%s\n' "${output}" + echo "__CZ_BUMP_PREVIEW__" + } >> "$GITHUB_OUTPUT" + - name: Build comment body + env: + STATUS: ${{ steps.dry-run.outputs.status }} + OUTPUT: ${{ steps.dry-run.outputs.output }} + run: | + { + echo "" + echo "## 🔍 Commitizen bump preview" + echo "" + case "${STATUS}" in + 0) + echo "Merging this PR will produce the following bump:" + echo "" + echo '```' + printf '%s\n' "${OUTPUT}" + echo '```' + ;; + 21) + echo "No commits in this PR are eligible for a version bump." + ;; + *) + echo "⚠️ \`cz bump --dry-run\` exited with status \`${STATUS}\`:" + echo "" + echo '```' + printf '%s\n' "${OUTPUT}" + echo '```' + ;; + esac + } > comment.md + - uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body-path: comment.md + body-includes: "" + edit-mode: replace +``` + +#### How it works + +- **Trigger**: `pull_request_target` runs in the context of the base + repository, which gives the workflow `pull-requests: write` permission + even for PRs from forks. The job only runs `cz bump --dry-run`, a + read-only command, so it does not execute any PR-controlled scripts. +- **Setup**: [`commitizen-tools/setup-cz`](https://github.com/commitizen-tools/setup-cz) + installs the Commitizen CLI; no language-specific build tooling is required. +- **Dry-run**: `cz bump --dry-run --yes` computes the next version and the + changelog entries that would be produced. Exit code `21` (`NoneIncrementExit`) + is treated as "no eligible bump" rather than a failure. +- **Sticky comment**: The hidden HTML marker `` + lets [`peter-evans/create-or-update-comment`](https://github.com/peter-evans/create-or-update-comment) + find and replace the previous preview on every push, instead of leaving a + growing trail of comments. + +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). + ### Publishing a Python package After a new version tag is created by the bump workflow, you can automatically publish your package to PyPI. From 52530b0382fa70208b3156a867c1c340f8bbe2b2 Mon Sep 17 00:00:00 2001 From: Tim Hsiung Date: Sat, 9 May 2026 16:49:41 +0800 Subject: [PATCH 2/2] 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> --- .github/workflows/pr-bump-preview.yml | 21 +++++++++++++----- docs/tutorials/github_actions.md | 31 +++++++++++++++++++++------ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-bump-preview.yml b/.github/workflows/pr-bump-preview.yml index b85123f34..4cd240e27 100644 --- a/.github/workflows/pr-bump-preview.yml +++ b/.github/workflows/pr-bump-preview.yml @@ -10,19 +10,30 @@ permissions: jobs: bump-preview: - if: ${{ github.event.pull_request.draft == false }} + # Skip drafts, and skip fork PRs entirely. `pull_request_target` runs with + # the base repo's GITHUB_TOKEN (write access to PR comments). `cz bump` + # can render Jinja templates from the checked-out workspace whenever + # `update_changelog_on_bump` is set in config, and the renderer is not + # sandboxed (FileSystemLoader('.')) — running it against fork-controlled + # files would risk RCE / token exfiltration. Same-repo PRs are written by + # collaborators who already have push access, so the same risk doesn't + # apply. + if: > + ${{ + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == + github.event.pull_request.base.repo.full_name + }} runs-on: ubuntu-latest steps: - # Using pull_request_target means GITHUB_TOKEN has write access to PR - # comments even for fork PRs. We deliberately only run `cz bump --dry-run` - # against the checked-out PR commits — no PR-controlled scripts are - # executed, so this is safe. - name: Check out PR head uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 fetch-tags: true + # Defense in depth: don't write the workflow token to .git/config. + persist-credentials: false - name: Set up Commitizen uses: commitizen-tools/setup-cz@main diff --git a/docs/tutorials/github_actions.md b/docs/tutorials/github_actions.md index 46b196b8d..7717bdbf5 100644 --- a/docs/tutorials/github_actions.md +++ b/docs/tutorials/github_actions.md @@ -127,7 +127,7 @@ You can find the complete workflow in our repository at [bumpversion.yml](https: To help reviewers spot unexpected version bumps before merging, you can run `cz bump --dry-run` on every pull request and post (or update) a sticky -comment summarizing the would-be version bump and changelog entries. +comment summarizing the would-be version bump. Create `.github/workflows/pr-bump-preview.yml`: @@ -144,7 +144,13 @@ permissions: jobs: bump-preview: - if: ${{ github.event.pull_request.draft == false }} + # Skip drafts and fork PRs (see "How it works" below). + if: > + ${{ + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == + github.event.pull_request.base.repo.full_name + }} runs-on: ubuntu-latest steps: - name: Check out PR head @@ -153,6 +159,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 fetch-tags: true + persist-credentials: false - uses: commitizen-tools/setup-cz@main with: set-git-config: false @@ -211,18 +218,30 @@ jobs: - **Trigger**: `pull_request_target` runs in the context of the base repository, which gives the workflow `pull-requests: write` permission - even for PRs from forks. The job only runs `cz bump --dry-run`, a - read-only command, so it does not execute any PR-controlled scripts. + even for PRs from forks. We deliberately gate the job to **same-repo PRs + only** (`head.repo == base.repo`); fork PRs are skipped. This is because + `cz bump` renders [Jinja templates from the working directory][jinja] + whenever [`update_changelog_on_bump`](../config/configuration_file.md) is + enabled, and the renderer is not sandboxed — running it against + fork-controlled files under a write token would risk arbitrary code + execution and token exfiltration. Same-repo PRs are written by + collaborators who already have push access, so the same risk doesn't + apply. - **Setup**: [`commitizen-tools/setup-cz`](https://github.com/commitizen-tools/setup-cz) installs the Commitizen CLI; no language-specific build tooling is required. -- **Dry-run**: `cz bump --dry-run --yes` computes the next version and the - changelog entries that would be produced. Exit code `21` (`NoneIncrementExit`) +- **Defense in depth**: `persist-credentials: false` on `actions/checkout` + keeps the workflow token out of the local git config. +- **Dry-run**: `cz bump --dry-run --yes` computes the next version (and, if + `update_changelog_on_bump` is set in your config, also the changelog + entries that would be produced). Exit code `21` (`NoneIncrementExit`) is treated as "no eligible bump" rather than a failure. - **Sticky comment**: The hidden HTML marker `` lets [`peter-evans/create-or-update-comment`](https://github.com/peter-evans/create-or-update-comment) find and replace the previous preview on every push, instead of leaving a growing trail of comments. +[jinja]: https://github.com/commitizen-tools/commitizen/blob/master/commitizen/changelog.py + 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). ### Publishing a Python package