Skip to content

Commit dc87355

Browse files
authored
feat: fork-PR comments via pull_request + workflow_run split (+ checkout@v7 docs hardening) (#142)
## What Reworks how bumpy gets a release-plan comment onto **fork** PRs. Two parts: 1. **Docs hardening (original scope):** lead with the unprivileged `pull_request` check as the default; apply the July-16 `actions/checkout@v7` change (`allow-unsafe-pr-checkout` on the read-only `./pr` checkout). 2. **New safer fork-comment architecture:** a `pull_request` + `workflow_run` split where the **privileged half never touches fork code or files** — added after we audited the residual risks of the single-file `pull_request_target` approach. ## Why the split A fork's `pull_request` token is read-only at issuance (enforced server-side — REST/GraphQL/`gh`/`curl` all 403, no secrets), so the comment must be posted from a privileged base-repo run. The `workflow_run` split is the only design where the privileged job has **no untrusted checkout, no package manager run against fork config, and reads no fork file** — it just posts pre-rendered text. That removes the `allow-unsafe-pr-checkout` fragility, the symlink read-surface, the registry-redirect surface, and the CodeQL dismissal that the `pull_request_target` workflow carries. ## CLI changes (`@varlock/bumpy`, minor) - **`ci check --emit-comment <dir>`** — renders the comment to `<dir>/comment.md` (seeds an empty file so the artifact is always present) instead of only posting it. Still posts directly when it has a write token (same-repo PRs). - **`ci comment --body-file <path>`** — posts a pre-rendered body. **Security-critical:** it resolves the target PR from the **trusted `workflow_run` event** (`head_sha` → PR lookup), *never* from the artifact (which is fork-influenced), and treats the body as data. Sets `GH_REPO` from `GITHUB_REPOSITORY` so it runs without a checkout. No-ops on a missing/empty body. ## User-facing setup The fork-comment setup is **optional** — only needed to show the comment on PRs from forks (the check itself already runs red/green on forks). Add `--emit-comment` + `upload-artifact` to your existing `pull_request` CI, and drop in a small `workflow_run` poster. Docs: [`Commenting on fork PRs`](https://github.com/dmno-dev/bumpy/blob/claude/adoring-buck-35a83a/docs/github-actions.md#commenting-on-fork-prs). The `pull_request_target` approach has been **removed** from the docs — the split is the only documented fork-comment path. ## Dogfood Replaced our `pull_request_target` `bumpy-check.yaml` (two jobs) with a single `pull_request` check that emits + uploads, plus a new `bumpy-comment.yaml` (`workflow_run`) that builds main's bumpy and posts. ## Tests / verification - New `test/core/ci-comment.test.ts`: `resolveTargetPrNumber` derives the PR from the event `head_sha` (and ignores a hostile `#999` in the body); `ci comment` no-ops on missing/empty body. Full suite: **389 pass**, typecheck + lint + fmt clean. - Smoke-tested `--emit-comment` (writes a real rendered `comment.md`) and `ci comment` (no-op + error paths) end-to-end. ## ⚠️ One-time bootstrapping note On *this* PR, the new dogfood `bumpy-comment.yaml` (which builds **main's** bumpy) will fail, because `ci comment` doesn't exist on `main` until this merges. That's cosmetic — it's the poster workflow, not the PR check, and this PR's own comment still posts directly from `bumpy-check.yaml` (same-repo write token). It goes green automatically once merged.
1 parent 3bff4dc commit dc87355

11 files changed

Lines changed: 439 additions & 189 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@varlock/bumpy': minor
3+
---
4+
5+
Add a `pull_request` + `workflow_run` option for commenting on fork PRs, so the privileged half never touches fork code. `bumpy ci check --emit-comment <dir>` renders the release-plan comment to `<dir>/comment.md` for upload as an artifact, and a new `bumpy ci comment --body-file <path>` posts it from a `workflow_run` job. The target PR is resolved from the trusted `workflow_run` event (`head_sha`), never from the (untrusted) artifact.

.github/workflows/bumpy-check.yaml

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,38 @@
1-
# 🐸 Bumpy CI check
2-
# checks for missing bump files and posts/updates a PR comment with the release plan
1+
# 🐸 Bumpy CI check (dogfood)
2+
# Runs our own unreleased bumpy on every PR and renders the release-plan comment as an
3+
# artifact. Runs on the UNPRIVILEGED `pull_request` event, so it's safe to build and run
4+
# the PR's own bumpy (fork or not) — there's no write token or secrets to protect here.
5+
# Posting the comment on fork PRs is the privileged half and lives in bumpy-comment.yaml
6+
# (workflow_run). A normal project just adds
7+
# bunx @varlock/bumpy ci check --emit-comment ./bumpy-comment
8+
# to its existing CI workflow.
39
#
4-
# ⚠️ NOTE - DO NOT COPY THIS FILE
5-
# instead look at the recommended workflow in the docs
10+
# ⚠️ DO NOT COPY THIS FILE — see the recommended setup in the docs:
611
# ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️
7-
#
8-
# This repo builds and runs its OWN unreleased bumpy so we dogfood the current
9-
# CLI on every PR. Two jobs, split by trust level:
10-
# - non-fork PRs build and run the PR's OWN bumpy, so a PR previews its own
11-
# ci-check changes. Safe because the code comes from this repo.
12-
# - fork PRs build and run MAIN's bumpy and only READ the PR via `--cwd ./pr`,
13-
# so no untrusted code ever touches the pull_request_target write token.
14-
# A normal project just runs `bunx @varlock/bumpy@latest ci check --cwd ./pr`.
15-
1612
name: Bumpy Check
1713

18-
on: pull_request_target # < necessary so it can post comments on fork PRs
14+
on: pull_request
1915

2016
permissions:
21-
pull-requests: write
17+
pull-requests: write # same-repo PRs comment directly; fork PRs are read-only (the poster handles those)
2218
contents: read
2319

2420
jobs:
25-
# Non-fork PRs (trusted): build and run the PR's OWN bumpy so it dogfoods its
26-
# own changes. `--cwd .` acknowledges that the current checkout is trusted.
27-
check-local:
28-
if: github.event.pull_request.head.repo.full_name == github.repository
21+
check:
2922
runs-on: ubuntu-latest
3023
steps:
31-
- uses: actions/checkout@v6
24+
- uses: actions/checkout@v7
3225
with:
33-
ref: ${{ github.event.pull_request.head.sha }}
3426
fetch-depth: 0 # history to diff bump files against the PR base branch
3527
- uses: oven-sh/setup-bun@v2
3628
- run: bun install
37-
# Build first since we run the local built version of bumpy.
3829
- run: bun run --filter @varlock/bumpy build
39-
# Re-install so the freshly-built CLI bin is linked.
40-
- run: bun install
41-
- run: bunx @varlock/bumpy ci check --cwd .
30+
- run: bun install # link the freshly-built CLI bin
31+
- run: bunx @varlock/bumpy ci check --emit-comment ./bumpy-comment
4232
env:
4333
GH_TOKEN: ${{ github.token }}
44-
45-
# Fork PRs (untrusted): build and run MAIN's bumpy from a trusted checkout and
46-
# only READ the PR head via `--cwd ./pr` — never install/build/run fork code.
47-
check-fork:
48-
if: github.event.pull_request.head.repo.full_name != github.repository
49-
runs-on: ubuntu-latest
50-
steps:
51-
# TRUSTED base checkout (main) — bumpy is built and run from here.
52-
- uses: actions/checkout@v6
53-
with:
54-
ref: main
55-
persist-credentials: false
56-
# UNTRUSTED PR head into ./pr — only READ via --cwd, never built or run.
57-
- uses: actions/checkout@v6
34+
- uses: actions/upload-artifact@v4
35+
if: always() # upload even when the check fails — the comment explains why
5836
with:
59-
ref: ${{ github.event.pull_request.head.sha }}
60-
path: pr
61-
persist-credentials: false
62-
- uses: oven-sh/setup-bun@v2
63-
- run: bun install
64-
- run: bun run --filter @varlock/bumpy build
65-
- run: bun install
66-
# bunx runs from the trusted root; bumpy reads the PR's bump files via --cwd.
67-
- run: bunx @varlock/bumpy ci check --cwd ./pr
68-
env:
69-
GH_TOKEN: ${{ github.token }}
37+
name: bumpy-comment
38+
path: ./bumpy-comment
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# 🐸 Bumpy PR comment (dogfood)
2+
# Privileged half of the fork-comment split: posts the release-plan comment that
3+
# bumpy-check.yaml rendered. Triggered after that workflow completes. It NEVER checks
4+
# out or runs PR code — it builds MAIN's bumpy and posts a pre-rendered body to the PR
5+
# it resolves from the trusted workflow_run.head_sha. A normal project would just run
6+
# `bunx @varlock/bumpy ci comment` instead of building from source.
7+
#
8+
# ⚠️ DO NOT COPY THIS FILE — see the recommended setup in the docs:
9+
# ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️
10+
name: Bumpy PR Comment
11+
12+
on:
13+
workflow_run:
14+
workflows: ['Bumpy Check']
15+
types: [completed]
16+
17+
permissions:
18+
pull-requests: write
19+
20+
jobs:
21+
comment:
22+
runs-on: ubuntu-latest
23+
steps:
24+
# TRUSTED: default branch (main) only — never the PR. Build our own bumpy.
25+
- uses: actions/checkout@v7
26+
- uses: oven-sh/setup-bun@v2
27+
- run: bun install
28+
- run: bun run --filter @varlock/bumpy build
29+
- run: bun install # link the freshly-built CLI bin
30+
# Download AFTER checkout — checkout cleans the workspace and would wipe it.
31+
- uses: actions/download-artifact@v4
32+
continue-on-error: true # the check may not have produced a comment
33+
with:
34+
name: bumpy-comment
35+
path: ./bumpy-comment
36+
run-id: ${{ github.event.workflow_run.id }}
37+
github-token: ${{ github.token }}
38+
- run: bunx @varlock/bumpy ci comment --body-file ./bumpy-comment/comment.md
39+
env:
40+
GH_TOKEN: ${{ github.token }}

.github/workflows/ci.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ jobs:
1414
- run: git config --global user.name "CI" && git config --global user.email "ci@example.com"
1515
- run: bun run test
1616

17-
# NOTE: `bumpy ci check` lives in .github/workflows/bumpy-check.yaml
17+
# NOTE: `bumpy ci check` runs in .github/workflows/bumpy-check.yaml (pull_request),
18+
# and the fork-PR comment is posted from .github/workflows/bumpy-comment.yaml (workflow_run).
1819
# see ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️

docs/cli.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,18 +184,35 @@ CI command for PR checks. Computes the release plan from bump files changed in t
184184
bumpy ci check
185185
bumpy ci check --strict
186186
bumpy ci check --no-fail
187+
bumpy ci check --emit-comment ./bumpy-comment
187188
```
188189

189-
| Flag | Description |
190-
| ----------- | ---------------------------------------------------------------- |
191-
| `--comment` | Force PR comment on or off (default: auto-detect CI environment) |
192-
| `--strict` | Fail if any changed package is not covered by a bump file |
193-
| `--no-fail` | Warn only, never exit non-zero |
190+
| Flag | Description |
191+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
192+
| `--comment` | Force PR comment on or off (default: auto-detect CI environment) |
193+
| `--strict` | Fail if any changed package is not covered by a bump file |
194+
| `--no-fail` | Warn only, never exit non-zero |
195+
| `--emit-comment <dir>` | Also write the rendered comment to `<dir>/comment.md` for a downstream [`ci comment`](#bumpy-ci-comment) to post (the fork-comment split) |
194196

195197
Requires `GH_TOKEN` environment variable (automatically available in GitHub Actions).
196198

197199
**On channel and promotion PRs:** for a PR targeting a [prerelease channel](prereleases.md) branch, the comment is channel-aware — it shows the prerelease plan (`-<preid>.x` versions) and the target dist-tag rather than implying a stable release. For a **promotion PR** (a channel branch → `main`, or a graduation like `alpha``beta`), the check reads the cycle's already-shipped bump files from `.bumpy/<channel>/` and shows the consolidated stable plan, calling out that merging ends the prerelease cycle. (Feature PRs that _target_ a channel branch are checked against that branch, so only the PR's own new bump files count — see [`--base`](#bumpy-check) on the local `check` command for the equivalent locally.)
198200

201+
## `bumpy ci comment`
202+
203+
Posts a pre-rendered comment (from `ci check --emit-comment`) to a PR. This is the privileged half of the [fork-comment split](github-actions.md#commenting-on-fork-prs): your `pull_request` check renders the comment as an artifact, and this command — run from a `workflow_run` job — posts it.
204+
205+
```bash
206+
bumpy ci comment --body-file ./bumpy-comment/comment.md
207+
```
208+
209+
| Flag | Description |
210+
| -------------------- | ------------------------------------------------------------------ |
211+
| `--body-file <path>` | Path to the rendered comment body (required) |
212+
| `--pr <number>` | Target PR number (default: resolved from the `workflow_run` event) |
213+
214+
It needs no checkout and no bumpy project — it only posts. Under `workflow_run` it resolves the target PR from the **trusted** event (`head_sha`), never from the artifact, and treats the body as untrusted text. A missing or empty body file is a no-op. Requires `GH_TOKEN`.
215+
199216
## `bumpy ci plan`
200217

201218
CI command that reports what `ci release` would do, without acting. Outputs JSON to stdout and sets GitHub Actions step outputs so you can conditionally run expensive steps (builds, etc.) only when needed.

0 commit comments

Comments
 (0)