Skip to content

Commit bb37d26

Browse files
theoephraimCI
andauthored
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>
1 parent 010a74e commit bb37d26

8 files changed

Lines changed: 226 additions & 54 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+
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).

.github/workflows/bumpy-check.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# 🐸 Bumpy CI check
2+
# checks for missing bump files and posts/updates a PR comment with the release plan
3+
4+
# ⚠️ NOTE - DO NOT COPY THIS FILE
5+
# instead look at the recommended workflow in the docs
6+
# ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️
7+
8+
name: Bumpy Check
9+
10+
on: pull_request_target # < necessary so it can post comments on fork PRs
11+
12+
permissions:
13+
pull-requests: write
14+
contents: read
15+
16+
jobs:
17+
bumpy-check:
18+
runs-on: ubuntu-latest
19+
steps:
20+
# Check out the PR head so bumpy can read the PR's bump files, config, and package.json
21+
# We never execute this code!
22+
- uses: actions/checkout@v6
23+
with:
24+
ref: ${{ github.event.pull_request.head.sha }}
25+
- uses: oven-sh/setup-bun@v2
26+
27+
# reads json/yaml files only, so it's safe to run on fork PRs
28+
- run: bunx @varlock/bumpy@latest ci check
29+
env:
30+
GH_TOKEN: ${{ github.token }}

.github/workflows/ci.yaml

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

17-
bumpy-check:
18-
runs-on: ubuntu-latest
19-
permissions:
20-
pull-requests: write
21-
steps:
22-
- uses: actions/checkout@v6
23-
- uses: oven-sh/setup-bun@v2
24-
- run: bun install
25-
26-
# --- You wont need this part ---
27-
# Build first since we use the local built version of bumpy instead of the published one
28-
- run: bun run --filter @varlock/bumpy build
29-
# run bun install again to make the now built CLI available
30-
- run: bun install
31-
# -------------------------------
32-
33-
# 🐸 This is the important part - checks for missing bump files and posts/updates a PR comment with the release plan
34-
- run: bunx @varlock/bumpy ci check
35-
env:
36-
GH_TOKEN: ${{ github.token }}
17+
# NOTE: `bumpy ci check` lives in .github/workflows/bumpy-check.yaml
18+
# see ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️

.github/workflows/on-release.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# simple workflow that runs after a release is published
2+
# used only to verify "release workflow -> further actions" is working
3+
14
name: On Release
25

36
on:

.github/workflows/release.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 🐸 Bumpy CI release
2+
# when changes are made to main, it either creates/updates release PR, or triggers release
3+
4+
# ⚠️ NOTE - DO NOT COPY THIS FILE
5+
# instead look at the recommended workflow in the docs
6+
# ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️
7+
18
name: Release
29
on:
310
push:
@@ -8,7 +15,7 @@ concurrency:
815
cancel-in-progress: false
916

1017
jobs:
11-
# Detect what `ci release` would do and gate downstream jobs accordingly.
18+
# Detect what `bumpy ci release` would do and gate downstream jobs accordingly.
1219
# Runs with no write permissions and no publish credentials.
1320
plan:
1421
runs-on: ubuntu-latest

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,40 @@ _examples use bun, but works with Node.js_
8888
### PR check workflow
8989

9090
```yaml
91-
# .github/workflows/bumpy-check.yml
91+
# .github/workflows/bumpy-check.yaml
92+
#
93+
# ⚠️ Uses `pull_request_target` so fork PR comments work — runs with write
94+
# perms and secrets, so it MUST NOT execute PR code (no `bun install`, no
95+
# PR-defined scripts). Bumpy only reads files; its version is resolved from
96+
# the base branch's package.json. See docs/github-actions.md for details.
9297
name: Bumpy Check
93-
on: pull_request
98+
on: pull_request_target
99+
100+
permissions:
101+
pull-requests: write
102+
contents: read
94103

95104
jobs:
96105
check:
97106
runs-on: ubuntu-latest
98-
permissions:
99-
pull-requests: write
100107
steps:
101108
- uses: actions/checkout@v6
109+
with:
110+
ref: ${{ github.event.pull_request.head.sha }}
102111
- uses: oven-sh/setup-bun@v2
103-
- run: bun install
104-
- run: bunx @varlock/bumpy ci check
112+
113+
# Resolve bumpy's version from the base branch (trusted) — not the PR's
114+
# package.json (which a fork PR could swap to a malicious version).
115+
# Change "main" to your base branch if different.
116+
- name: Resolve bumpy version from base
117+
run: |
118+
git fetch origin main --depth=1
119+
VERSION=$(git show "origin/main:package.json" \
120+
| jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' \
121+
| sed 's/[\^~]//')
122+
echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"
123+
124+
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check
105125
env:
106126
GH_TOKEN: ${{ github.token }}
107127
```

docs/github-actions.md

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,103 @@
22

33
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.
44

5-
## Overview
5+
These commands facilitate the following:
66

7-
| Command | Trigger | What it does |
8-
| ------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
9-
| `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)
14+
> - `bun install``npm ci` / `pnpm install --frozen-lockfile` / `yarn install --immutable`
15+
> - `bunx @varlock/bumpy@…``npx @varlock/bumpy@…` / `pnpm dlx @varlock/bumpy@…` / `yarn dlx @varlock/bumpy@…`
16+
>
17+
> 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.
1218
1319
## PR check workflow
1420

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+
1523
```yaml
16-
# .github/workflows/bumpy-check.yml
24+
# .github/workflows/bumpy-check.yaml
1725
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
1932

2033
jobs:
2134
check:
2235
runs-on: ubuntu-latest
23-
permissions:
24-
pull-requests: write
2536
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.
2639
- uses: actions/checkout@v6
40+
with:
41+
ref: ${{ github.event.pull_request.head.sha }}
2742
- 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.
54+
git fetch origin main --depth=1
55+
VERSION=$(git show "origin/main:package.json" \
56+
| jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' \
57+
| sed 's/[\^~]//')
58+
echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"
59+
60+
# Quote the version arg so a malformed value can't shell-inject.
61+
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check
3062
env:
3163
GH_TOKEN: ${{ github.token }}
3264
```
3365
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+
34102
## Release workflow (recommended: split jobs)
35103

36104
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:
48116
49117
jobs:
50118
# 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.
51120
plan:
52121
runs-on: ubuntu-latest
53122
permissions:
54123
contents: read
55124
outputs:
56125
mode: ${{ steps.plan.outputs.mode }}
57126
packages: ${{ steps.plan.outputs.packages }}
127+
bumpy_version: ${{ steps.bumpy-version.outputs.version }}
58128
steps:
59129
- uses: actions/checkout@v6
60130
with:
61131
fetch-depth: 0
62132
- uses: oven-sh/setup-bun@v2
63-
- run: bun install
133+
# No `bun install` — bumpy reads files (package.jsons, bump files) and doesn't need your workspace deps resolved
134+
# We just pin its version from package.json and let bunx fetch it
135+
- id: bumpy-version
136+
name: Resolve bumpy version
137+
run: |
138+
VERSION=$(jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' package.json | sed 's/[\^~]//')
139+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
140+
echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"
64141
- id: plan
65-
run: bunx @varlock/bumpy ci plan
142+
run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci plan
66143
env:
67144
GH_TOKEN: ${{ github.token }}
68145

@@ -74,13 +151,14 @@ jobs:
74151
permissions:
75152
contents: write
76153
pull-requests: write
154+
env:
155+
BUMPY_VERSION: ${{ needs.plan.outputs.bumpy_version }}
77156
steps:
78157
- uses: actions/checkout@v6
79158
with:
80159
fetch-depth: 0
81160
- uses: oven-sh/setup-bun@v2
82-
- run: bun install
83-
- run: bunx @varlock/bumpy ci release --expect-mode version-pr
161+
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci release --expect-mode version-pr
84162
env:
85163
GH_TOKEN: ${{ github.token }}
86164
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # so the version PR triggers CI
@@ -94,6 +172,8 @@ jobs:
94172
permissions:
95173
contents: write
96174
id-token: write # required for npm trusted publishing (OIDC) and provenance
175+
env:
176+
BUMPY_VERSION: ${{ needs.plan.outputs.bumpy_version }}
97177
steps:
98178
- uses: actions/checkout@v6
99179
with:
@@ -103,18 +183,19 @@ jobs:
103183
with:
104184
node-version: latest
105185
- run: npm install -g npm@latest # ensure npm >= 11.15.0 for OIDC/staged publishing
106-
- run: bun install
107-
# 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
110191
env:
111192
GH_TOKEN: ${{ github.token }}
112193
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # so `release: published` workflows trigger
113194
```
114195
115196
**How the three jobs interact:**
116197
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.
118199
- Only one of `version-pr` or `publish` runs per push. The other is skipped via the `if:` condition.
119200
- 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.
120201
- 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

Comments
 (0)