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/pat-pr-and-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@varlock/bumpy': minor
---

Add `--pat-pr` and `--pat-comments` flags to CI commands, allowing users to opt in to using `BUMPY_GH_TOKEN` for PR creation and comment posting. Also fixes CI not triggering on newly created version PRs.
11 changes: 9 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@ jobs:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- run: bun install
# Build first since we use the local workspace package instead of the published one

# --- 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
- run: bunx @varlock/bumpy ci check --fail-on-missing
# -------------------------------

# 🐸 This is the important part - checks for missing bump files and posts/updates a PR comment with the release plan
# (use --pat-comments if PAT belongs to dedicated bot, rather than person)
- run: bunx @varlock/bumpy ci check --fail-on-missing --pat-comments
env:
GH_TOKEN: ${{ github.token }}
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # <- PAT needed so that release PR will trigger CI
16 changes: 11 additions & 5 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@ jobs:
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
# Node.js is needed for npm publish (trusted publishing via OIDC)
# Node.js (npm) is needed for npm publish
- uses: actions/setup-node@v6
with:
node-version: lts/*
node-version: lts/* # newer node needed for OIDC
- run: bun install
# Build first since we use the local workspace package instead of the published one

# --- 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
- run: bunx @varlock/bumpy ci release
# -------------------------------

# 🐸 This is the important part - creates/updates release PR when PRs merge to main, publishes packages when release PR is merged
# (use --pat-pr if PAT belongs to dedicated bot, rather than person)
- run: bunx @varlock/bumpy ci release --pat-pr
env:
GH_TOKEN: ${{ github.token }}
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }}
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }} # <- PAT needed so that release PR will trigger CI
6 changes: 4 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,9 @@ bumpy ci check --fail-on-missing
| ------------------- | ---------------------------------------------------------------- |
| `--comment` | Force PR comment on or off (default: auto-detect CI environment) |
| `--fail-on-missing` | Exit 1 if changed packages have no bump files |
| `--pat-comments` | Post PR comments using `BUMPY_GH_TOKEN` instead of `GH_TOKEN` |

Requires `GH_TOKEN` environment variable.
Requires `GH_TOKEN` environment variable. The `--pat-comments` flag requires `BUMPY_GH_TOKEN` — use it when the token belongs to a dedicated automation account (bot user). If you're using a developer's personal PAT, leave this off so comments appear from `github-actions[bot]`.

## `bumpy ci release`

Expand All @@ -157,8 +158,9 @@ bumpy ci release --auto-publish --tag beta
| `--auto-publish` | Version + publish directly instead of creating a PR |
| `--tag <tag>` | npm dist-tag (for `--auto-publish`) |
| `--branch <name>` | Version PR branch name (default: `bumpy/version-packages`) |
| `--pat-pr` | Create/edit the version PR using `BUMPY_GH_TOKEN` |

Requires `GH_TOKEN`. Optionally uses `BUMPY_GH_TOKEN` to create PRs that trigger other workflows (see [GitHub Actions setup](github-actions.md#token-setup)).
Requires `GH_TOKEN`. Optionally uses `BUMPY_GH_TOKEN` to push the version branch so PR workflows trigger (see [GitHub Actions setup](github-actions.md#token-setup)). The `--pat-pr` flag additionally uses `BUMPY_GH_TOKEN` to create/edit the PR itself — use it when the token belongs to a dedicated automation account (bot user). If you're using a developer's personal PAT, leave this off so the PR is authored by `github-actions[bot]` and the developer can still approve it.

## `bumpy ci setup`

Expand Down
25 changes: 19 additions & 6 deletions docs/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,20 @@ The default `${{ github.token }}` provides the basic permissions needed for both

GitHub's anti-recursion guard prevents PRs created by the default `github.token` from triggering other workflows. This means your regular CI workflows (tests, linting, etc.) won't run automatically on the Version Packages PR — so you can't verify that the version bumps don't break anything before merging.

To fix this, provide a `BUMPY_GH_TOKEN` using either a **fine-grained PAT** or a **GitHub App token**. Bumpy uses this token to push the version branch and create the PR, which allows your CI workflows to trigger normally.
To fix this, provide a `BUMPY_GH_TOKEN` using either a **fine-grained PAT** or a **GitHub App token**. Bumpy uses this token to push the version branch, which allows your CI workflows to trigger normally.

#### Controlling what the token is used for

By default, `BUMPY_GH_TOKEN` is only used for git push operations. You can opt in to using it for other GitHub API calls:

| Flag | Command | Effect |
| ---------------- | ------------ | --------------------------------------------- |
| `--pat-pr` | `ci release` | Create/edit the version PR as the token owner |
| `--pat-comments` | `ci check` | Post PR comments as the token owner |

**When to use these flags:** Use them when `BUMPY_GH_TOKEN` belongs to a dedicated automation account (bot user or GitHub App). The PR and comments will appear from that account.

**When NOT to use these flags:** If you're using a developer's personal PAT, leave these off. PRs and comments will appear from `github-actions[bot]`, which allows the developer to still review and approve the PR.

Run `bumpy ci setup` for interactive guidance, or set it up manually:

Expand Down Expand Up @@ -162,8 +175,8 @@ A classic npm access token. Create one at [npmjs.com → Access Tokens](https://

## Environment variables summary

| Variable | Required | Used by | Description |
| ---------------- | ----------------- | ------------------------ | ------------------------------------------------- |
| `GH_TOKEN` | Yes | `ci check`, `ci release` | GitHub token for API access |
| `BUMPY_GH_TOKEN` | Recommended | `ci release` | PAT or App token so version PRs trigger workflows |
| `NPM_TOKEN` | If not using OIDC | `ci release` | npm access token for publishing |
| Variable | Required | Used by | Description |
| ---------------- | ----------------- | ------------------------ | ----------------------------------------------------------------- |
| `GH_TOKEN` | Yes | `ci check`, `ci release` | GitHub token for API access |
| `BUMPY_GH_TOKEN` | Recommended | `ci check`, `ci release` | PAT or App token — used for push, and optionally for PRs/comments |
| `NPM_TOKEN` | If not using OIDC | `ci release` | npm access token for publishing |
10 changes: 6 additions & 4 deletions llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,11 @@ Default flow: detects PM → packs tarball (resolves workspace:/catalog: protoco

PR check — reports pending bump files and optionally comments on the PR with the release plan.

| Flag | Description |
| ------------------- | ------------------------------------------------------------- |
| `--comment` | Force PR commenting on/off (auto-detected in CI environments) |
| `--fail-on-missing` | Exit 1 if no bump files found |
| Flag | Description |
| ------------------- | --------------------------------------------------------------------- |
| `--comment` | Force PR commenting on/off (auto-detected in CI environments) |
| `--fail-on-missing` | Exit 1 if no bump files found |
| `--pat-comments` | Post PR comments using `BUMPY_GH_TOKEN` instead of default `GH_TOKEN` |

Auto-detects PR number from `GITHUB_REF` in GitHub Actions. Also checks `BUMPY_PR_NUMBER` and `PR_NUMBER` env vars.

Expand All @@ -365,6 +366,7 @@ Release automation — either creates a "Version Packages" PR or auto-publishes
| `--auto-publish` | Version + publish directly instead of creating a PR |
| `--tag <tag>` | npm dist-tag for auto-publish mode |
| `--branch <name>` | Branch name for version PR (default: `bumpy/version-packages`) |
| `--pat-pr` | Create/edit the version PR using `BUMPY_GH_TOKEN` |

Default mode (`version-pr`): creates a branch, runs `bumpy version`, commits, and opens/updates a PR via `gh`. Merging that PR triggers publish.

Expand Down
2 changes: 2 additions & 0 deletions packages/bumpy/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ async function main() {
await ciCheckCommand(rootDir, {
comment: ciFlags.comment !== undefined ? ciFlags.comment === true : undefined,
failOnMissing: ciFlags['fail-on-missing'] === true,
patComments: ciFlags['pat-comments'] === true,
});
} else if (subcommand === 'release') {
const { ciReleaseCommand } = await import('./commands/ci.ts');
Expand All @@ -113,6 +114,7 @@ async function main() {
mode,
tag: ciFlags.tag as string | undefined,
branch: ciFlags.branch as string | undefined,
patPr: ciFlags['pat-pr'] === true,
});
} else if (subcommand === 'setup') {
const { ciSetupCommand } = await import('./commands/ci-setup.ts');
Expand Down
83 changes: 68 additions & 15 deletions packages/bumpy/src/commands/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,41 @@ import { detectPackageManager } from '../utils/package-manager.ts';
import { createHash } from 'node:crypto';
import type { BumpyConfig, BumpFile, PackageManager, ReleasePlan, PlannedRelease } from '../types.ts';

// ---- PAT-scoped gh helpers ----

/**
* Temporarily override GH_TOKEN with BUMPY_GH_TOKEN for a gh CLI call.
*
* Use `--pat-pr` / `--pat-comments` flags to opt in. This is useful when
* BUMPY_GH_TOKEN belongs to a dedicated automation/bot account. If you're
* using a developer's personal PAT, it's better to leave these flags off so
* that PRs and comments appear from github-actions[bot] — allowing the
* developer to still review and approve the PR.
*/
function requirePatToken(): string {
const token = process.env.BUMPY_GH_TOKEN;
if (!token) {
throw new Error('BUMPY_GH_TOKEN must be set when using --pat-pr or --pat-comments');
}
return token;
}

async function withPatToken<T>(usePat: boolean, fn: () => Promise<T>): Promise<T> {
if (!usePat) return fn();
const token = requirePatToken();
const originalGhToken = process.env.GH_TOKEN;
process.env.GH_TOKEN = token;
try {
return await fn();
} finally {
if (originalGhToken !== undefined) {
process.env.GH_TOKEN = originalGhToken;
} else {
delete process.env.GH_TOKEN;
}
}
}

// ---- Validation helpers ----

/** Validate a git branch name to prevent injection */
Expand Down Expand Up @@ -45,6 +80,7 @@ function ensureGitIdentity(rootDir: string, config: BumpyConfig): void {
interface CheckOptions {
comment?: boolean; // post a PR comment via gh (default: true in CI)
failOnMissing?: boolean; // exit 1 if no bump files (default: false)
patComments?: boolean; // post PR comments using BUMPY_GH_TOKEN
}

/**
Expand Down Expand Up @@ -84,7 +120,7 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi

if (shouldComment && prNumber) {
const prBranch = detectPrBranch(rootDir);
await postOrUpdatePrComment(prNumber, formatNoBumpFilesComment(prBranch, pm), rootDir);
await postOrUpdatePrComment(prNumber, formatNoBumpFilesComment(prBranch, pm), rootDir, opts.patComments);
}

if (opts.failOnMissing) {
Expand All @@ -111,7 +147,7 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
if (shouldComment && prNumber) {
const prBranch = detectPrBranch(rootDir);
const comment = formatReleasePlanComment(plan, prBumpFiles, prNumber, prBranch, pm, plan.warnings);
await postOrUpdatePrComment(prNumber, comment, rootDir);
await postOrUpdatePrComment(prNumber, comment, rootDir, opts.patComments);
}
}

Expand All @@ -121,6 +157,7 @@ interface ReleaseOptions {
mode: 'auto-publish' | 'version-pr';
tag?: string; // npm dist-tag for auto-publish
branch?: string; // branch name for version PR (default: "bumpy/version-packages")
patPr?: boolean; // create/edit PR using BUMPY_GH_TOKEN
}

/**
Expand Down Expand Up @@ -153,7 +190,7 @@ export async function ciReleaseCommand(rootDir: string, opts: ReleaseOptions): P
await autoPublish(rootDir, config, opts.tag);
} else {
const packageDirs = new Map([...packages.values()].map((p) => [p.name, p.relativeDir]));
await createVersionPr(rootDir, plan, config, packageDirs, opts.branch);
await createVersionPr(rootDir, plan, config, packageDirs, opts.branch, opts.patPr);
}
}

Expand Down Expand Up @@ -263,6 +300,7 @@ async function createVersionPr(
config: BumpyConfig,
packageDirs: Map<string, string>,
branchName?: string,
patPr?: boolean,
): Promise<void> {
const branch = validateBranchName(branchName || config.versionPr.branch);
const baseBranch = validateBranchName(
Expand Down Expand Up @@ -310,19 +348,30 @@ async function createVersionPr(
if (existingPr) {
const validPr = validatePrNumber(existingPr);
log.step(`Updating existing PR #${validPr}...`);
await runArgsAsync(['gh', 'pr', 'edit', validPr, '--title', config.versionPr.title, '--body-file', '-'], {
cwd: rootDir,
input: prBody,
});
await withPatToken(!!patPr, () =>
runArgsAsync(['gh', 'pr', 'edit', validPr, '--title', config.versionPr.title, '--body-file', '-'], {
cwd: rootDir,
input: prBody,
}),
);
log.success(`Updated PR #${validPr}`);
} else {
log.step('Creating version PR...');
const prTitle = config.versionPr.title;
const result = await runArgsAsync(
['gh', 'pr', 'create', '--title', prTitle, '--body-file', '-', '--base', baseBranch, '--head', branch],
{ cwd: rootDir, input: prBody },
const result = await withPatToken(!!patPr, () =>
runArgsAsync(
['gh', 'pr', 'create', '--title', prTitle, '--body-file', '-', '--base', baseBranch, '--head', branch],
{ cwd: rootDir, input: prBody },
),
);
log.success(`Created PR: ${result}`);
if (!patPr) {
// Push again with the custom token now that the PR exists, so that a
// `pull_request: synchronize` event is generated and CI workflows trigger.
// (The initial push happened before the PR existed, and the PR creation
// event from GITHUB_TOKEN doesn't trigger workflows.)
pushWithToken(rootDir, branch);
}
}

// Switch back to the base branch
Expand Down Expand Up @@ -525,7 +574,7 @@ function formatVersionPrBody(plan: ReleasePlan, preamble: string, packageDirs: M

const COMMENT_MARKER = '<!-- bumpy-release-plan -->';

async function postOrUpdatePrComment(prNumber: string, body: string, rootDir: string): Promise<void> {
async function postOrUpdatePrComment(prNumber: string, body: string, rootDir: string, usePat = false): Promise<void> {
const validPr = validatePrNumber(prNumber);
const markedBody = `${COMMENT_MARKER}\n${body}`;

Expand All @@ -540,13 +589,17 @@ async function postOrUpdatePrComment(prNumber: string, body: string, rootDir: st
const commentId = existingComment?.split('\n')[0]?.trim();

if (commentId) {
await runArgsAsync(
['gh', 'api', `repos/{owner}/{repo}/issues/comments/${commentId}`, '-X', 'PATCH', '-F', 'body=@-'],
{ cwd: rootDir, input: markedBody },
await withPatToken(usePat, () =>
runArgsAsync(
['gh', 'api', `repos/{owner}/{repo}/issues/comments/${commentId}`, '-X', 'PATCH', '-F', 'body=@-'],
{ cwd: rootDir, input: markedBody },
),
);
log.dim(' Updated PR comment');
} else {
await runArgsAsync(['gh', 'pr', 'comment', validPr, '--body-file', '-'], { cwd: rootDir, input: markedBody });
await withPatToken(usePat, () =>
runArgsAsync(['gh', 'pr', 'comment', validPr, '--body-file', '-'], { cwd: rootDir, input: markedBody }),
);
log.dim(' Posted PR comment');
}
} catch (err) {
Expand Down