Skip to content

Commit 5782ff9

Browse files
authored
Split-job release workflow + --mode assertion flag (#91)
## Summary - Adds `--expect-mode <version-pr|publish>` to `bumpy ci release` for asserting the detected release mode (errors instead of silently smart-routing when the runtime state doesn't match what the job expects). Cannot be combined with `--auto-publish`. - Restructures the project's own release workflow into three jobs: a low-privilege `plan` job gating a `version-pr` job (PR-write creds only) and a `publish` job (`environment: publish`, `id-token: write` for npm trusted publishing). - Rewrites [docs/github-actions.md](docs/github-actions.md) to lead with the split-job workflow + setup steps. The single-job version is kept as a simplified alternative. ## Why Without a split, every push to main runs a job with `id-token: write` (or `NPM_TOKEN` in env) — even when the workflow is only updating the Version Packages PR. The split scopes those credentials to the publish job only and lets the npm trusted publisher be pinned to a specific GitHub Environment so rogue workflow files in the repo can't request a valid OIDC token. The `--expect-mode` flag turns the split's implicit `if:` gate into an explicit runtime assertion: if the publish job somehow runs while bump files are still present (a race, weird merge, or someone manually re-triggers it), it fails fast instead of silently falling into the version-pr code path. ## Setup required after merge 1. **Pin the npm trusted publisher to environment `publish`** on each package's npmjs.com settings → Trusted Publishers → GitHub Actions. _(Already done.)_ 2. **(Optional) Create the `publish` environment manually** in repo Settings → Environments and restrict deployment branches to `main` only. This is cheap defense in depth — auto-create works too if you skip this. _(Already done.)_ The new docs framing makes the split clearer: - **Required setup**: npm trusted publisher pinning + `BUMPY_GH_TOKEN`. That's it. - **Optional hardening**: branch restriction on the environment (recommended), required reviewers (usually redundant with `npmStaged: true`). ## Notable internals - `ReleaseOptions.mode: 'auto-publish' | 'version-pr'` renamed to `autoPublish: boolean` — cleaner CLI-flag-to-internal mapping. New `assertMode?: 'version-pr' | 'publish'` field carries the assertion. The only caller is [packages/bumpy/src/cli.ts](packages/bumpy/src/cli.ts). - `ciReleaseCommand` now computes `detectedMode` once and consolidates the two near-identical "fall through to publish" branches. - Auto-publish mode flagged as not recommended in the docs (loses the review step + can't compose with the split-job pattern). - The user-facing flag is `--expect-mode` (reads natural in YAML); the internal field stays `assertMode` (technically accurate at the code level). ## Test plan - [x] `bun run check` — typecheck + format clean - [x] `bun run test` — 225 tests pass - [ ] Manual: confirm the new release workflow runs end-to-end on the next merge to main (will exercise the `plan` → `version-pr` path here, since this PR itself ships with a bump file) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent afd465b commit 5782ff9

7 files changed

Lines changed: 261 additions & 133 deletions

File tree

.bumpy/ci-expect-mode-flag.md

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 `--expect-mode` flag to `bumpy ci release` for asserting the detected release mode (`version-pr` or `publish`). Enables split-job release workflows where each job fails loudly if the runtime state doesn't match what the job expects. Refactored `ReleaseOptions` to rename the existing `mode` field to `autoPublish: boolean` and add `assertMode`. `--expect-mode` and `--auto-publish` cannot be combined.

.github/workflows/release.yaml

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,20 @@ concurrency:
88
cancel-in-progress: false
99

1010
jobs:
11-
release:
11+
# Detect what `ci release` would do and gate downstream jobs accordingly.
12+
# Runs with no write permissions and no publish credentials.
13+
plan:
1214
runs-on: ubuntu-latest
1315
permissions:
14-
contents: write
15-
pull-requests: write
16-
id-token: write # required for npm trusted publishing (OIDC)
16+
contents: read
17+
outputs:
18+
mode: ${{ steps.plan.outputs.mode }}
19+
packages: ${{ steps.plan.outputs.packages }}
1720
steps:
1821
- uses: actions/checkout@v6
1922
with:
2023
fetch-depth: 0
2124
- uses: oven-sh/setup-bun@v2
22-
# Node.js (npm) is needed for npm publish
23-
- uses: actions/setup-node@v6
24-
with:
25-
node-version: latest
2625
- run: bun install
2726

2827
# --- You wont need this part ---
@@ -32,21 +31,69 @@ jobs:
3231
- run: bun install
3332
# -------------------------------
3433

35-
# 🐸 Plan first — detects mode and caches the result for ci release
36-
# Outputs: mode (version-pr|publish|nothing), packages (comma-separated), json (full plan)
34+
# 🐸 Outputs: mode (version-pr|publish|nothing), packages (comma-separated), json (full plan)
3735
- id: plan
3836
run: bunx @varlock/bumpy ci plan
3937
env:
4038
GH_TOKEN: ${{ github.token }}
4139

42-
# Example: conditionally run expensive steps only when publishing
43-
# In your project, this is where you'd put build/compile/test steps
44-
# that are only needed before a publish (not when updating the version PR)
45-
- if: steps.plan.outputs.mode == 'publish'
46-
run: echo "📦 Publish mode — packages to release:" && echo "${{ steps.plan.outputs.packages }}"
40+
# Creates/updates the Version Packages PR. No publish credentials — never sees
41+
# id-token or npm secrets, so a malicious commit to main can't ride this job to publish.
42+
version-pr:
43+
needs: plan
44+
if: needs.plan.outputs.mode == 'version-pr'
45+
runs-on: ubuntu-latest
46+
permissions:
47+
contents: write
48+
pull-requests: write
49+
steps:
50+
- uses: actions/checkout@v6
51+
with:
52+
fetch-depth: 0
53+
- uses: oven-sh/setup-bun@v2
54+
- run: bun install
4755

48-
# Creates/updates release PR when PRs merge to main, publishes packages when release PR is merged
49-
- run: bunx @varlock/bumpy ci release
56+
# --- You wont need this part ---
57+
- run: bun run --filter @varlock/bumpy build
58+
- run: bun install
59+
# -------------------------------
60+
61+
- run: bunx @varlock/bumpy ci release --expect-mode version-pr
5062
env:
5163
GH_TOKEN: ${{ github.token }}
5264
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # <- PAT so that version PR triggers CI
65+
66+
# Publishes packages. Scoped to the `publish` environment — pin the npm trusted
67+
# publisher to this environment name on npmjs.com so that an OIDC token requested
68+
# from any other job (or a rogue workflow file) will be rejected by npm.
69+
publish:
70+
needs: plan
71+
if: needs.plan.outputs.mode == 'publish'
72+
runs-on: ubuntu-latest
73+
environment: publish
74+
permissions:
75+
contents: write
76+
id-token: write # required for npm trusted publishing (OIDC)
77+
steps:
78+
- uses: actions/checkout@v6
79+
with:
80+
fetch-depth: 0
81+
- uses: oven-sh/setup-bun@v2
82+
# Node.js (npm) is needed for npm publish
83+
- uses: actions/setup-node@v6
84+
with:
85+
node-version: latest
86+
- run: bun install
87+
88+
# --- You wont need this part ---
89+
- run: bun run --filter @varlock/bumpy build
90+
- run: bun install
91+
# -------------------------------
92+
93+
- run: echo "📦 Publishing packages:" && echo "${{ needs.plan.outputs.packages }}"
94+
95+
- run: bunx @varlock/bumpy ci release --expect-mode publish
96+
env:
97+
GH_TOKEN: ${{ github.token }}
98+
# We dont use the default GH token so that further workflows can be triggred by GH release events
99+
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }}

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,6 @@ jobs:
171171

172172
</details>
173173

174-
You can also use `bumpy ci release --auto-publish` to version + publish directly on merge without the intermediate PR.
175-
176174
### Token setup
177175

178176
The default `github.token` works for basic functionality, but GitHub's anti-recursion guard means PRs created by the default token won't trigger other workflows - so your regular CI (tests, linting, etc.) won't run automatically on the Version Packages PR. To fix this, provide a `BUMPY_GH_TOKEN` secret using either a **fine-grained PAT** or a **GitHub App token**. See the [full token setup guide](https://github.com/dmno-dev/bumpy/blob/main/docs/github-actions.md#token-setup) for details.

docs/cli.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,19 +223,20 @@ CI command for releases. Has two modes:
223223

224224
**Version PR mode (default):** If pending bump files exist, creates or updates a "Version Packages" PR with all version bumps and changelog updates. If the current push is the Version Packages PR being merged, publishes the new versions, creates git tags, and creates GitHub releases.
225225

226-
**Auto-publish mode (`--auto-publish`):** Versions and publishes directly on merge without an intermediate PR.
226+
**Auto-publish mode (`--auto-publish`):** Versions and publishes directly on merge without an intermediate PR. **Not recommended** — you lose the version-PR preview/review gate, so every merge to main with a bump file ships immediately. It's also incompatible with the [split-job workflow](github-actions.md#release-workflow-recommended-split-jobs) (since both paths happen in one run). The credential surface itself is the same as a single-job non-auto-publish workflow — the cost here is purely the loss of the preview gate.
227227

228228
```bash
229229
bumpy ci release
230230
bumpy ci release --auto-publish
231231
bumpy ci release --auto-publish --tag beta
232232
```
233233

234-
| Flag | Description |
235-
| ----------------- | ---------------------------------------------------------- |
236-
| `--auto-publish` | Version + publish directly instead of creating a PR |
237-
| `--tag <tag>` | npm dist-tag (for `--auto-publish`) |
238-
| `--branch <name>` | Version PR branch name (default: `bumpy/version-packages`) |
234+
| Flag | Description |
235+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
236+
| `--expect-mode <mode>` | Assert detected mode: `version-pr` or `publish`. Errors if the detected mode differs. Use to gate split-job workflows so a job can't silently fall into the wrong path. |
237+
| `--auto-publish` | Version + publish directly instead of creating a PR |
238+
| `--tag <tag>` | npm dist-tag (for `--auto-publish`) |
239+
| `--branch <name>` | Version PR branch name (default: `bumpy/version-packages`) |
239240

240241
Requires `GH_TOKEN`. When `BUMPY_GH_TOKEN` is set, it is automatically used to push the version branch and create/edit the PR so that PR workflows trigger (see [GitHub Actions setup](github-actions.md#token-setup)).
241242

0 commit comments

Comments
 (0)