Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .bumpy/pull-request-target-check-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@varlock/bumpy': minor
---

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).
30 changes: 30 additions & 0 deletions .github/workflows/bumpy-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 🐸 Bumpy CI check
# checks for missing bump files and posts/updates a PR comment with the release plan

# ⚠️ NOTE - DO NOT COPY THIS FILE
# instead look at the recommended workflow in the docs
# ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️

name: Bumpy Check

on: pull_request_target # < necessary so it can post comments on fork PRs

permissions:
pull-requests: write
contents: read

jobs:
bumpy-check:
runs-on: ubuntu-latest
steps:
# Check out the PR head so bumpy can read the PR's bump files, config, and package.json
# We never execute this code!
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: oven-sh/setup-bun@v2

# reads json/yaml files only, so it's safe to run on fork PRs
- run: bunx @varlock/bumpy@latest ci check
env:
GH_TOKEN: ${{ github.token }}
22 changes: 2 additions & 20 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,5 @@ jobs:
- run: git config --global user.name "CI" && git config --global user.email "ci@example.com"
- run: bun run test

bumpy-check:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- run: bun install

# --- You wont need this part ---
# Build first since we use the local built version of bumpy instead of the published one
- run: bun run --filter @varlock/bumpy build
# run bun install again to make the now built CLI available
- run: bun install
# -------------------------------

# 🐸 This is the important part - checks for missing bump files and posts/updates a PR comment with the release plan
- run: bunx @varlock/bumpy ci check
env:
GH_TOKEN: ${{ github.token }}
# NOTE: `bumpy ci check` lives in .github/workflows/bumpy-check.yaml
# see ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️
3 changes: 3 additions & 0 deletions .github/workflows/on-release.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# simple workflow that runs after a release is published
# used only to verify "release workflow -> further actions" is working

name: On Release

on:
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 🐸 Bumpy CI release
# when changes are made to main, it either creates/updates release PR, or triggers release

# ⚠️ NOTE - DO NOT COPY THIS FILE
# instead look at the recommended workflow in the docs
# ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️

name: Release
on:
push:
Expand All @@ -8,7 +15,7 @@ concurrency:
cancel-in-progress: false

jobs:
# Detect what `ci release` would do and gate downstream jobs accordingly.
# Detect what `bumpy ci release` would do and gate downstream jobs accordingly.
# Runs with no write permissions and no publish credentials.
plan:
runs-on: ubuntu-latest
Expand Down
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,40 @@ _examples use bun, but works with Node.js_
### PR check workflow

```yaml
# .github/workflows/bumpy-check.yml
# .github/workflows/bumpy-check.yaml
#
# ⚠️ Uses `pull_request_target` so fork PR comments work — runs with write
# perms and secrets, so it MUST NOT execute PR code (no `bun install`, no
# PR-defined scripts). Bumpy only reads files; its version is resolved from
# the base branch's package.json. See docs/github-actions.md for details.
name: Bumpy Check
on: pull_request
on: pull_request_target

permissions:
pull-requests: write
contents: read

jobs:
check:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bunx @varlock/bumpy ci check

# Resolve bumpy's version from the base branch (trusted) — not the PR's
# package.json (which a fork PR could swap to a malicious version).
# Change "main" to your base branch if different.
- name: Resolve bumpy version from base
run: |
git fetch origin main --depth=1
VERSION=$(git show "origin/main:package.json" \
| jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' \
| sed 's/[\^~]//')
echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"

- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check
env:
GH_TOKEN: ${{ github.token }}
```
Expand Down
123 changes: 102 additions & 21 deletions docs/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,103 @@

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.

## Overview
These commands facilitate the following:

| Command | Trigger | What it does |
| ------------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bumpy ci check` | `pull_request` | Posts/updates a PR comment with the release plan. Warns about missing bump files. |
| `bumpy ci plan` | `push` to main | Reports what `ci release` would do (JSON + GitHub Actions outputs). Use to gate downstream jobs. |
| `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). |
- **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
- **When a regular PR merges to main** - create/update a special "release PR" which updates changelogs and version numbers, and deletes the bump files
- **When release PR is merged** - trigger the release process

> **Using npm / pnpm / yarn instead of bun?** All examples below use `bun` / `bunx` for brevity, but bumpy itself is package-manager agnostic. Substitute:
>
> - `oven-sh/setup-bun@v2` → `actions/setup-node@v6` (+ `pnpm/action-setup` if using pnpm)
> - `bun install` → `npm ci` / `pnpm install --frozen-lockfile` / `yarn install --immutable`
> - `bunx @varlock/bumpy@…` → `npx @varlock/bumpy@…` / `pnpm dlx @varlock/bumpy@…` / `yarn dlx @varlock/bumpy@…`
>
> 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.

## PR check workflow

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).

```yaml
# .github/workflows/bumpy-check.yml
# .github/workflows/bumpy-check.yaml
name: Bumpy Check
on: pull_request

on: pull_request_target # so it can post comments on fork PRs

permissions:
pull-requests: write
contents: read

jobs:
check:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
# Check out the PR head so bumpy can read the PR's bump files, config,
# and package.json. We never execute this code.
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bunx @varlock/bumpy ci check

# ⚠️ DO NOT INSTALL DEPS OR EXECUTE CODE ⚠️

# Resolve bumpy's version from the BASE branch's package.json (trusted).
# Reading it from the PR's package.json would let a fork PR swap in a
# malicious version of bumpy.
- name: Resolve bumpy version from base
run: |
# Hardcoded to "main" rather than ${{ github.event.pull_request.base.ref }}
# because the PR controls its base — pointing at any other branch you have
# would read that branch's package.json. Change "main" to your base branch.
git fetch origin main --depth=1
VERSION=$(git show "origin/main:package.json" \
| jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' \
| sed 's/[\^~]//')
echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"

# Quote the version arg so a malformed value can't shell-inject.
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci check
env:
GH_TOKEN: ${{ github.token }}
```

### ⚠️ Security: no installs, no PR scripts

`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:

- **No `bun install` / `npm install`** — postinstall scripts execute as PR code, and a malicious PR can add or modify dependencies.
- **No `bun run <script>` / `npm test`** — the script body comes from the PR's `package.json`.
- **No building from the PR tree** — same problem.

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.

### How the bumpy version stays in sync

`git show origin/main:package.json | jq …` reads bumpy's version from `main` at workflow runtime. That means:

- **No version pinned in the workflow file** — Renovate/Dependabot bumps to `package.json` flow through automatically.
- **Fork PRs can't swap the bumpy version** — the source of truth is `main`, which they don't control.

A few things to adjust if your setup is different:

- If your default branch isn't `main`, change the two `origin/main` references to your base branch.
- If `@varlock/bumpy` lives somewhere other than root `package.json` (e.g. a sub-package), point the `git show` path at that file instead.

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.

### Don't need fork PR support?

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:

```yaml
- run: bunx @varlock/bumpy ci check
env:
GH_TOKEN: ${{ github.token }}
```

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).

## Release workflow (recommended: split jobs)

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.
Expand All @@ -48,21 +116,30 @@ concurrency:

jobs:
# Detect what `ci release` would do — no write permissions, no publish credentials.
# Also resolves bumpy's version once and exposes it as an output for downstream jobs.
plan:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
mode: ${{ steps.plan.outputs.mode }}
packages: ${{ steps.plan.outputs.packages }}
bumpy_version: ${{ steps.bumpy-version.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- run: bun install
# No `bun install` — bumpy reads files (package.jsons, bump files) and doesn't need your workspace deps resolved
# We just pin its version from package.json and let bunx fetch it
- id: bumpy-version
name: Resolve bumpy version
run: |
VERSION=$(jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' package.json | sed 's/[\^~]//')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "BUMPY_VERSION=$VERSION" >> "$GITHUB_ENV"
- id: plan
run: bunx @varlock/bumpy ci plan
run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci plan
env:
GH_TOKEN: ${{ github.token }}

Expand All @@ -74,13 +151,14 @@ jobs:
permissions:
contents: write
pull-requests: write
env:
BUMPY_VERSION: ${{ needs.plan.outputs.bumpy_version }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bunx @varlock/bumpy ci release --expect-mode version-pr
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci release --expect-mode version-pr
env:
GH_TOKEN: ${{ github.token }}
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # so the version PR triggers CI
Expand All @@ -94,6 +172,8 @@ jobs:
permissions:
contents: write
id-token: write # required for npm trusted publishing (OIDC) and provenance
env:
BUMPY_VERSION: ${{ needs.plan.outputs.bumpy_version }}
steps:
- uses: actions/checkout@v6
with:
Expand All @@ -103,18 +183,19 @@ jobs:
with:
node-version: latest
- run: npm install -g npm@latest # ensure npm >= 11.15.0 for OIDC/staged publishing
- run: bun install
# Expensive build steps that only matter before publish go here:
# - run: bun run build
- run: bunx @varlock/bumpy ci release --expect-mode publish
# Build steps that need to happen before publish go here. If your build
# needs workspace deps, add `bun install` first:
# - run: bun install
# - run: bun run build
- run: bunx "@varlock/bumpy@$BUMPY_VERSION" ci release --expect-mode publish
env:
GH_TOKEN: ${{ github.token }}
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # so `release: published` workflows trigger
```

**How the three jobs interact:**

- `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.
- `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.
- Only one of `version-pr` or `publish` runs per push. The other is skipped via the `if:` condition.
- 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.
- Expensive build steps (compilation, tests, bundling) only run inside the `publish` job, so PR merges that just maintain the version PR stay cheap.
Expand Down
Loading