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
7 changes: 7 additions & 0 deletions .bumpy/dependency-update-bump-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@varlock/bumpy': minor
---

Add `bumpy ci deps` for Dependabot / Renovate PRs (modeled on changesets-dependencies-action). It diffs each package's `dependencies` / `peerDependencies` / `optionalDependencies` (plus `releaseTriggeringDevDeps`, and catalog updates) against the base branch, writes one patch bump file per affected package listing the added / updated / removed deps, and commits + pushes them to the PR branch. Re-running is idempotent and removes stale files. `bumpy generate --deps` does the same locally without committing.

Also fix `ci check` on PRs into a channel branch: the `package.json` field diff and catalog diff compared against `baseBranch` instead of the PR's base, so dependency drift already merged into the channel was attributed to every later PR.
25 changes: 20 additions & 5 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ Auto-create bump files from commits on the current branch. Works with any commit
bumpy generate
bumpy generate --dry-run
bumpy generate --from v1.0.0 # override: scan from a specific ref instead of branch base
bumpy generate --deps # one patch bump file per package whose dependencies changed
```

| Flag | Description |
| --------------- | ------------------------------------------------------------------------ |
| `--from <ref>` | Git ref to scan from (default: branch point from `baseBranch` in config) |
| `--dry-run` | Preview without creating files |
| `--name <name>` | Bump file filename |
| Flag | Description |
| --------------- | ------------------------------------------------------------------------------------------- |
| `--from <ref>` | Git ref to scan from (default: branch point from `baseBranch` in config) |
| `--dry-run` | Preview without creating files |
| `--name <name>` | Bump file filename (with `--deps`: filename suffix, default: current branch) |
| `--deps` | Generate from dependency changes instead of commits — see [`bumpy ci deps`](#bumpy-ci-deps) |

**How commits are mapped:**

Expand Down Expand Up @@ -213,6 +215,19 @@ bumpy ci comment --body-file ./bumpy-comment/comment.md

It needs no checkout and no bumpy project — it only posts. Under `workflow_run` it resolves the target PR from the **trusted** event (`head_sha`), never from the artifact, and treats the body as untrusted text. A missing or empty body file is a no-op. Requires `GH_TOKEN`.

## `bumpy ci deps`

For dependency-update PRs (Dependabot, Renovate). Writes one patch bump file per package whose `dependencies` / `peerDependencies` / `optionalDependencies` (or [`releaseTriggeringDevDeps`](configuration.md#release-triggering-devdependencies)) changed, as `.bumpy/deps-pr<N>-<pkg>.md`, then commits and pushes them to the PR branch. Idempotent — re-running rewrites the files and removes stale ones. See [Dependency update PRs](github-actions.md#dependency-update-prs-dependabot--renovate) for the workflow.

```bash
bumpy ci deps
bumpy ci deps --no-push
```

| Flag | Description |
| ----------- | ------------------------------------ |
| `--no-push` | Commit the bump files but don't push |

## `bumpy ci plan`

CI command that reports what `ci release` would do, without acting. Outputs JSON to stdout and sets GitHub Actions step outputs so you can conditionally run expensive steps (builds, etc.) only when needed.
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ To relax additional fields (e.g. treat `scripts` changes as non-releasing too),
}
```

For runtime dependency updates from bots, [`bumpy ci deps`](github-actions.md#dependency-update-prs-dependabot--renovate) can write the bump files automatically.

bumpy errs toward requiring a bump file whenever it can't compare cleanly — a brand-new `package.json`, or one it can't parse.

### Dependency bump rules
Expand Down
51 changes: 51 additions & 0 deletions docs/github-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,57 @@ Point `workflows: [...]` at the **name** of whatever runs your check (your exist

> **Why can't the fork run post it itself?** A fork's `pull_request` token is read-only at issuance and enforced server-side — REST, GraphQL, `gh`, and raw `curl` all 403 on a comment write, and secrets aren't exposed either. The write has to originate from a privileged base-repo run, which is exactly what `workflow_run` provides.

## Dependency update PRs (Dependabot / Renovate)

A dev-only update (`devDependencies`) never needs a bump file — see [change detection](configuration.md#change-detection-and-packagejson-fields). An update to `dependencies`, `peerDependencies` or `optionalDependencies` does, since it changes what your consumers install. Rather than adding those by hand, `bumpy ci deps` writes them for you (the bumpy equivalent of [changesets-dependencies-action](https://github.com/the-guild-org/changesets-dependencies-action)):

- diffs each managed package's dependencies against the base branch — the same changes `bumpy check` requires a bump file for (catalog updates included; `devDependencies` only if listed in [`releaseTriggeringDevDeps`](configuration.md#release-triggering-devdependencies))
- writes one **patch** bump file per affected package, `.bumpy/deps-pr<N>-<pkg>.md`, listing each added / updated / removed dependency with a link to npm
- for a [`directBump: false`](configuration.md#directbump-false--packages-that-only-follow) package, puts the bump on a fixed-group member instead (and fails if there isn't one)
- commits and pushes them to the PR branch — re-running (e.g. after Dependabot rebases) rewrites the files and deletes ones that no longer apply. Generated files carry a marker comment; files without it are never touched

Run it before `ci check`, in the same job, so the check sees the new files:

```yaml
# .github/workflows/bumpy-deps.yaml
name: Bumpy (dependency PRs)
on: pull_request

jobs:
deps:
# gate on the PR author, not github.actor — so a maintainer's push to a bot PR
# (e.g. pinning or reverting the update) regenerates the files too
if: contains(fromJSON('["dependabot[bot]", "renovate[bot]"]'), github.event.pull_request.user.login)
runs-on: ubuntu-latest
permissions:
contents: write # push the bump-file commit
pull-requests: write # ci check comment
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- run: bunx @varlock/bumpy ci deps
env:
GH_TOKEN: ${{ github.token }}
BUMPY_GH_TOKEN: ${{ secrets.BUMPY_GH_TOKEN }}
- run: bunx @varlock/bumpy ci check
env:
GH_TOKEN: ${{ github.token }}
```

Then skip bumpy's regular PR check for those PRs (the same condition, negated), or leave it — it will pass on the re-run triggered by the pushed commit. The re-run of this workflow finds the files up to date and commits nothing, so it doesn't loop.

Things to know:

- **Use `BUMPY_GH_TOKEN` if checks are required.** A commit pushed with the default `GITHUB_TOKEN` doesn't trigger new workflow runs, so required status checks on the new head commit stay pending. Workflows triggered by Dependabot only see **Dependabot secrets** — add `BUMPY_GH_TOKEN` under _Settings → Secrets and variables → Dependabot_ as well as _Actions_. See [Token setup](#token-setup).
- **Dependabot stops auto-rebasing PRs that have commits from someone else.** Comment `@dependabot recreate` to get a fresh branch; the workflow then regenerates the bump files.
- **Fork PRs are skipped** — their branch can't be pushed to.
- **PRs into a channel branch** are diffed against that branch (`GITHUB_BASE_REF`), so only this PR's dependency changes get bump files.
- `ci deps` sets a `changed` step output (`true` / `false`).
- Locally, `bumpy generate --deps` writes the same files (named after the branch) without committing.

## 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 Down
16 changes: 13 additions & 3 deletions packages/bumpy/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ async function main() {
from: flags.from as string | undefined,
dryRun: flags['dry-run'] === true,
name: flags.name as string | undefined,
deps: flags.deps === true,
});
break;
}
Expand Down Expand Up @@ -157,6 +158,9 @@ async function main() {
noFail: ciFlags['no-fail'] === true,
emitComment: typeof emitComment === 'string' ? emitComment : undefined,
});
} else if (subcommand === 'deps') {
const { ciDepsCommand } = await import('./commands/ci.ts');
await ciDepsCommand(rootDir, { noPush: ciFlags['no-push'] === true });
} else if (subcommand === 'plan') {
const { ciPlanCommand } = await import('./commands/ci.ts');
await ciPlanCommand(rootDir);
Expand Down Expand Up @@ -198,7 +202,7 @@ async function main() {
await ciSetupCommand(rootDir);
} else {
log.error(
`Unknown ci subcommand: ${subcommand}. Use "ci check", "ci comment", "ci plan", "ci release", or "ci setup".`,
`Unknown ci subcommand: ${subcommand}. Use "ci check", "ci comment", "ci deps", "ci plan", "ci release", or "ci setup".`,
);
process.exit(1);
}
Expand Down Expand Up @@ -263,7 +267,7 @@ function printHelp() {
add Create a new bump file
--none Set all changed packages to "none" (acknowledge without bumping)
--empty Create an empty bump file (no releases needed)
generate Generate bump file from branch commits
generate Generate bump file from branch commits (or dependency changes with --deps)
status Show pending releases
check Verify changed packages have bump files (for git hooks)
--strict Fail if any changed package is uncovered (default: only fail if no bump files at all)
Expand All @@ -277,6 +281,7 @@ function printHelp() {
(--snapshot <name>: transient preview publish to a throwaway dist-tag)
ci check PR check — report pending releases, comment on PR
ci comment Post a pre-rendered comment (workflow_run half of the fork-comment split)
ci deps Dependabot/Renovate PRs — commit patch bump files for dependency updates
ci plan Report what ci release would do (JSON + GitHub Actions outputs)
ci release Release — create version PR or auto-publish
ci setup Set up a token for triggering CI on version PRs
Expand All @@ -290,7 +295,12 @@ function printHelp() {
Generate options:
--from <ref> Git ref to scan from (default: branch point from baseBranch)
--dry-run Preview without creating a bump file
--name <name> Bump file filename
--name <name> Bump file filename (with --deps: file name suffix, default: branch name)
--deps One patch bump file per package whose dependencies changed
(dependencies/peerDependencies/optionalDependencies + releaseTriggeringDevDeps)

CI deps options:
--no-push Write + commit the bump files but don't push

Status options:
--json Output as JSON (includes dirs, bumpFiles, packageNames)
Expand Down
13 changes: 9 additions & 4 deletions packages/bumpy/src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export async function checkCommand(rootDir: string, opts: CheckOptions = {}): Pr
}
}

const changedPackages = await findChangedPackages(changedFiles, packages, rootDir, config);
const changedPackages = await findChangedPackages(changedFiles, packages, rootDir, config, baseBranch);

if (changedPackages.length === 0) {
log.info('No managed packages have changed.');
Expand Down Expand Up @@ -281,12 +281,17 @@ export function resolveDirectBumpCoverage(
return { missing: stillMissing, hints };
}

/** Map changed files to the packages they belong to */
/**
* Map changed files to the packages they belong to. `compareBranch` must be the branch
* `changedFiles` was computed against (e.g. a channel branch for a PR into it), so the
* package.json field diff and catalog diff use the same base.
*/
export async function findChangedPackages(
changedFiles: string[],
packages: Map<string, WorkspacePackage>,
rootDir: string,
config: BumpyConfig,
compareBranch: string = config.baseBranch,
): Promise<string[]> {
const changed = new Set<string>();

Expand Down Expand Up @@ -331,7 +336,7 @@ export async function findChangedPackages(
// package.json was the only thing that matched — only flag if a publish-affecting
// field actually changed (a dev-only dependency bump shouldn't require a release).
if (!changed.has(name) && pkgJsonOnlyTrigger) {
baseRef ??= getBaseCompareRef(rootDir, config.baseBranch);
baseRef ??= getBaseCompareRef(rootDir, compareBranch);
if (
await packageJsonAffectsRelease(rootDir, baseRef, pkgRelDir, ignoredFields, pkg.bumpy?.releaseTriggeringDevDeps)
) {
Expand All @@ -342,7 +347,7 @@ export async function findChangedPackages(

// Catalog change detection: if a catalog file changed, find packages whose
// catalog: dep references resolve to a changed catalog entry
const catalogChanges = await getChangedCatalogEntries(rootDir, config.baseBranch, changedFiles);
const catalogChanges = await getChangedCatalogEntries(rootDir, compareBranch, changedFiles);
if (catalogChanges.size > 0) {
for (const [name, pkg] of packages) {
if (changed.has(name)) continue;
Expand Down
Loading
Loading