You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: use pull_request_target for ci check workflow (#97)
## Summary
- Switches the **recommended** PR-check workflow from `pull_request` to
`pull_request_target` so the release-plan comment posts on fork PRs
(previously failed silently with a red ❌ and no comment — bad first
impression for OSS contributors).
- `bumpy ci check` now reads PR number from `GITHUB_EVENT_PATH`, so it
works under both `pull_request` and `pull_request_target`.
- When comment posting fails on a fork PR running under `pull_request`,
surfaces an actionable warning pointing at the new docs (otherwise
contributors see a red check with no clue why).
- Splits `bumpy ci check` out of the main `ci.yaml` into its own
`bumpy-check.yaml` workflow on `pull_request_target` (bumpy's own
dogfood variant — uses `@latest` since `workspace:*` can't be resolved
via the docs pattern).
- Reworks the release workflow docs:
- `plan` and `version-pr` jobs no longer `bun install` — bumpy only
reads files, doesn't need workspace deps resolved. Version is resolved
from `package.json` via `jq` instead.
- `plan` exposes `bumpy_version` as a job output; `version-pr` and
`publish` consume it via job-level `env:`, so there's a single
resolution step per workflow run.
- `publish` job still allows opt-in `bun install` for users with build
steps.
- Hardens the docs example workflow against the few non-zero attack
surfaces: hardcoded `origin/main` (closes the `base.ref`-control vector
— PR can target any branch in your repo, so we don't trust `${{
github.event.pull_request.base.ref }}`), and quoted
`"@varlock/bumpy@$BUMPY_VERSION"` in `bunx` (defense-in-depth against
shell injection through a malformed version string).
- Adds a callout in the docs about substituting `npm` / `pnpm` / `yarn`
for `bun` in the examples.
## Security model
The `pull_request_target` workflow runs with write tokens and access to
secrets even on fork PRs. The new docs example is designed around the
constraint that it must never execute PR-controlled code:
- No `bun install` (postinstall scripts execute PR code)
- No `bun run <script>` (script body comes from PR's `package.json`)
- No building from the PR tree
- Bumpy itself only reads files (markdown bump files, JSON config,
`package.json`)
- The bumpy version is resolved from the **base branch's**
`package.json` via `git show`, so a fork PR can't swap to a malicious
version
## Test plan
- [x] `bun run test` — all 258 tests pass
- [x] `bun run check` (oxlint + oxfmt + tsc) — clean
- [ ] On merge, the new `bumpy-check.yaml` workflow runs on subsequent
PRs (this one will run with the OLD logic since `pull_request_target`
reads workflows from base)
- [ ] Confirm fork-PR detection hint emits on a `pull_request`-trigger
workflow when a fork PR is opened without bump files (manual / future
verification)
---------
Co-authored-by: CI <ci@example.com>
Recommend `pull_request_target` for the `bumpy ci check` workflow so fork PRs receive release-plan comments. Previously, fork PRs running under `pull_request` got a read-only token, so the check would fail red with no helpful comment — a bad first impression for OSS projects. `bumpy ci check` now recognizes the `pull_request_target` event when reading the PR number from `GITHUB_EVENT_PATH`, and emits a clearer warning that links to the new docs when comment posting fails on a fork PR. See the updated [GitHub Actions docs](https://bumpy.varlock.dev/docs/github-actions) for the new workflow (the version is resolved from the base branch's `package.json`, so no version pinning duplication).
Bumpy handles CI automation through its `bumpy ci` subcommands — no separate GitHub Action or bot to install. Just call `bumpy ci` directly in your workflows.
|`bumpy ci check`|`pull_request`| Posts/updates a PR comment with the release plan. Warns about missing bump files. |
10
-
|`bumpy ci plan`|`push` to main | Reports what `ci release` would do (JSON + GitHub Actions outputs). Use to gate downstream jobs. |
11
-
|`bumpy ci release`|`push` to main | Either creates/updates the "Version Packages" PR (if bump files are present) or publishes packages, tags, and GitHub releases (if just versioned). |
7
+
-**On every PR** - check that PRs have bump files, add/update a comment with the release plan, outlining which packages will be bumped from the PR
8
+
-**When a regular PR merges to main** - create/update a special "release PR" which updates changelogs and version numbers, and deletes the bump files
9
+
-**When release PR is merged** - trigger the release process
10
+
11
+
> **Using npm / pnpm / yarn instead of bun?** All examples below use `bun` / `bunx` for brevity, but bumpy itself is package-manager agnostic. Substitute:
12
+
>
13
+
> -`oven-sh/setup-bun@v2` → `actions/setup-node@v6` (+ `pnpm/action-setup` if using pnpm)
> The version-resolution shell snippets work as-is regardless of package manager — they only depend on `jq` and `git`, both preinstalled on GitHub-hosted runners.
12
18
13
19
## PR check workflow
14
20
21
+
Posts/updates the release-plan comment on every PR, including PRs from forks. Adapt as needed — but **do not add an install step or run any PR-defined scripts** (see the security note after the example).
22
+
15
23
```yaml
16
-
# .github/workflows/bumpy-check.yml
24
+
# .github/workflows/bumpy-check.yaml
17
25
name: Bumpy Check
18
-
on: pull_request
26
+
27
+
on: pull_request_target # so it can post comments on fork PRs
28
+
29
+
permissions:
30
+
pull-requests: write
31
+
contents: read
19
32
20
33
jobs:
21
34
check:
22
35
runs-on: ubuntu-latest
23
-
permissions:
24
-
pull-requests: write
25
36
steps:
37
+
# Check out the PR head so bumpy can read the PR's bump files, config,
38
+
# and package.json. We never execute this code.
26
39
- uses: actions/checkout@v6
40
+
with:
41
+
ref: ${{ github.event.pull_request.head.sha }}
27
42
- uses: oven-sh/setup-bun@v2
28
-
- run: bun install
29
-
- run: bunx @varlock/bumpy ci check
43
+
44
+
# ⚠️ DO NOT INSTALL DEPS OR EXECUTE CODE ⚠️
45
+
46
+
# Resolve bumpy's version from the BASE branch's package.json (trusted).
47
+
# Reading it from the PR's package.json would let a fork PR swap in a
48
+
# malicious version of bumpy.
49
+
- name: Resolve bumpy version from base
50
+
run: |
51
+
# Hardcoded to "main" rather than ${{ github.event.pull_request.base.ref }}
52
+
# because the PR controls its base — pointing at any other branch you have
53
+
# would read that branch's package.json. Change "main" to your base branch.
# Quote the version arg so a malformed value can't shell-inject.
61
+
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check
30
62
env:
31
63
GH_TOKEN: ${{ github.token }}
32
64
```
33
65
66
+
### ⚠️ Security: no installs, no PR scripts
67
+
68
+
`pull_request_target` runs with write permissions and access to secrets — even on fork PRs. That's what lets us post comments on PRs from forks, but it means the workflow must never execute code that a PR author controls. In practice:
69
+
70
+
- **No `bun install` / `npm install`** — postinstall scripts execute as PR code, and a malicious PR can add or modify dependencies.
71
+
- **No `bun run <script>` / `npm test`** — the script body comes from the PR's `package.json`.
72
+
- **No building from the PR tree** — same problem.
73
+
74
+
Bumpy itself only reads files (markdown bump files, JSON config, `package.json`), so it's safe to run against the PR's source. The version is resolved from the base branch's `package.json` rather than the PR's, so a fork PR can't swap `@varlock/bumpy` to a malicious package.
75
+
76
+
### How the bumpy version stays in sync
77
+
78
+
`git show origin/main:package.json | jq …` reads bumpy's version from `main` at workflow runtime. That means:
79
+
80
+
- **No version pinned in the workflow file** — Renovate/Dependabot bumps to `package.json` flow through automatically.
81
+
- **Fork PRs can't swap the bumpy version** — the source of truth is `main`, which they don't control.
82
+
83
+
A few things to adjust if your setup is different:
84
+
85
+
- If your default branch isn't `main`, change the two `origin/main` references to your base branch.
86
+
- If `@varlock/bumpy` lives somewhere other than root `package.json` (e.g. a sub-package), point the `git show` path at that file instead.
87
+
88
+
You can also pin the bumpy version directly in the workflow (`bunx @varlock/bumpy@1.2.3 ci check`), but we prefer a single source of truth.
89
+
90
+
### Don't need fork PR support?
91
+
92
+
If you don't care about posting comments on external/fork PRs (private repo, internal-only contributors, etc.), you can skip the separate workflow entirely. Just add a step to your existing `pull_request` CI workflow:
93
+
94
+
```yaml
95
+
- run: bunx @varlock/bumpy ci check
96
+
env:
97
+
GH_TOKEN: ${{ github.token }}
98
+
```
99
+
100
+
Make sure the job has `permissions: pull-requests: write`. Since `pull_request` runs in a non-privileged context, all the "no installs / no PR scripts" rules above don't apply — you can `bun install` and run bumpy from your devDeps like any other CLI. The trade-off: fork PRs won't get a comment (the check still runs and fails red on missing bump files, just without the helpful explanation).
101
+
34
102
## Release workflow (recommended: split jobs)
35
103
36
104
The recommended release workflow splits version-PR maintenance from publishing into separate jobs. Only the publish job carries `id-token: write` and npm credentials, and it runs inside a GitHub Environment — so a rogue workflow elsewhere in the repo can't request an OIDC token that npm will accept.
@@ -48,21 +116,30 @@ concurrency:
48
116
49
117
jobs:
50
118
# Detect what `ci release` would do — no write permissions, no publish credentials.
119
+
# Also resolves bumpy's version once and exposes it as an output for downstream jobs.
# Expensive build steps that only matter before publish go here:
108
-
# - run: bun run build
109
-
- run: bunx @varlock/bumpy ci release --expect-mode publish
186
+
# Build steps that need to happen before publish go here. If your build
187
+
# needs workspace deps, add `bun install` first:
188
+
# - run: bun install
189
+
# - run: bun run build
190
+
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci release --expect-mode publish
110
191
env:
111
192
GH_TOKEN: ${{ github.token }}
112
193
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # so `release: published` workflows trigger
113
194
```
114
195
115
196
**How the three jobs interact:**
116
197
117
-
- `plan` runs `bumpy ci plan` to determine whether the current push should update the Version Packages PR (`version-pr`), publish unpublished packages (`publish`), or do nothing.
198
+
- `plan` runs `bumpy ci plan` to determine whether the current push should update the Version Packages PR (`version-pr`), publish unpublished packages (`publish`), or do nothing. It also resolves bumpy's version from `package.json` and exposes it as the `bumpy_version` output so downstream jobs don't have to re-resolve.
118
199
- Only one of `version-pr` or `publish` runs per push. The other is skipped via the `if:` condition.
119
200
- The `--expect-mode` flag on `ci release` asserts that the detected mode matches what each job expects — if the runtime state ever drifts, the job fails loudly instead of silently doing the wrong thing.
120
201
- Expensive build steps (compilation, tests, bundling) only run inside the `publish` job, so PR merges that just maintain the version PR stay cheap.
0 commit comments