
Bump Release
- 2.2k installs
- 68 repo stars
- Updated July 30, 2026
- paulrberg/agent-skills
bump-release is an agent skill that Use for release versioning: bump/cut/tag a release, bump version, create a release, changelog updates, or version taggin.
About
Release one package or several packages with version bumps changelog entries commits and tags Supports single package repos workspace monorepos regular releases beta releases and dry runs packages Optional monorepo package names or directories such as evm or evm safe Omit in single package repos version Optional explicit semver such as 2 0 0 Only valid for one target package beta Create or advance a beta X prerelease dry run Preview the release plan without modifying files committing or tagging Run the bundled planner before manual inspection It is read only and gives one JSON fact base for package discovery previous tags scoped changed files dependency edges and dirty tree status In pnpm workspaces it uses pnpm list r depth 1 json when available and falls back to local workspace glob discovery Bun and npm style package json workspaces use the local glob discovery including negative workspace patterns sh node CLAUDE_SKILL_DIR scripts plan release mjs cwd repo beta dry run version semver package name or dir For Codex or other
- argument-hint: '[packages...] [version] [--beta] [--dry-run]'
- description: 'Use for release versioning: bump/cut/tag a release, bump version, create a release, changelog updates, or
- Release one package or several packages with version bumps, changelog entries, commits, and tags. Supports single-packag
- Follow bump-release SKILL.md steps and documented constraints.
- Follow bump-release SKILL.md steps and documented constraints.
Bump Release by the numbers
- 2,180 all-time installs (skills.sh)
- +64 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #477 of 16,565 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
bump-release capabilities & compatibility
- Capabilities
- argument hint: '[packages...] [version] [ beta] · description: 'use for release versioning: bump/c · release one package or several packages with ver · follow bump release skill.md steps and documente
- Use cases
- orchestration
What bump-release says it does
argument-hint: '[packages...] [version] [--beta] [--dry-run]'
description: 'Use for release versioning: bump/cut/tag a release, bump version, create a release, changelog updates, or version tagging.'
Release one package or several packages with version bumps, changelog entries, commits, and tags. Supports single-package repos, workspace monorepos, regular releases, beta releases, and dry runs.
npx skills add https://github.com/paulrberg/agent-skills --skill bump-releaseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.2k |
|---|---|
| repo stars | ★ 68 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 30, 2026 |
| Repository | paulrberg/agent-skills ↗ |
When should an agent use bump-release and what problem does it solve?
Use for release versioning: bump/cut/tag a release, bump version, create a release, changelog updates, or version tagging.
Who is it for?
Developers invoking bump-release as documented in the skill source.
Skip if: Skip when requirements fall outside bump-release documented scope.
When should I use this skill?
Use for release versioning: bump/cut/tag a release, bump version, create a release, changelog updates, or version tagging.
What you get
Outputs aligned with the bump-release SKILL.md workflow and stated deliverables.
- CHANGELOG.md release section
- Human-readable release summary
Files
Bump Release
Release one package or several packages with version bumps, changelog entries, commits, and tags. Supports single-package repos, workspace monorepos, regular releases, beta releases, and dry runs.
Arguments
packages: Optional monorepo package names or directories, such asevmorevm-safe. Omit in single-package repos.version: Optional explicit semver, such as2.0.0. Only valid for one target package.--beta: Create or advance a-beta.Xprerelease.--dry-run: Preview the release plan without modifying files, committing, or tagging.
Fast Planner
Run the bundled planner before manual inspection. It is read-only and gives one JSON fact base for package discovery, previous tags, scoped changed files, dependency edges, and dirty-tree status. In pnpm workspaces, it uses pnpm list -r --depth -1 --json when available and falls back to local workspace-glob discovery. Bun and npm-style package.json workspaces use the local glob discovery, including negative workspace patterns.
For Claude Code:
node "${CLAUDE_SKILL_DIR}/scripts/plan-release.mjs" [--cwd <repo>] [--beta] [--dry-run] [--version <semver>] [--package <name-or-dir>]...For Codex or other agents, resolve <skill-dir> from the loaded SKILL.md path:
node "<skill-dir>/scripts/plan-release.mjs" [--cwd <repo>] [--beta] [--dry-run] [--version <semver>] [--package <name-or-dir>]...Map user arguments directly:
- Pass every package selector as
--package <selector>. - Pass an explicit version as
--version <semver>. - Pass
--betaand--dry-runwhen requested.
If the helper exits 2, stop: the cwd is not a git repo or has no root package.json. If it exits 64, read the JSON errors when present, report the invalid arguments, and stop.
Workflow
01. Run the planner - Use the JSON output as the source of truth for mode, packages, targets, previousTags, changedFiles, includedFiles, excludedFiles, dependencyEdges, needsSelection, and workingTree. 02. Require a clean tree - If workingTree.clean is false, stop and show the short status. Do not invoke the commit skill or commit unrelated work unless the user explicitly asks. 03. Resolve targets - If needsSelection is true, ask the user which workspace packages to release. If package selectors are unknown or ambiguous, stop and ask for exact package names or directories. 04. Reject invalid version scope - If an explicit version was supplied for more than one target package, stop. Explicit versions are single-package only. 05. Plan versions - Determine a candidate version for each target package:
- Explicit
version: use it as-is for a regular release; with--beta, append-beta.1unless it already has a prerelease suffix. - Beta from stable
1.2.3: use1.2.4-beta.1. - Beta from beta
1.2.3-beta.1: use1.2.3-beta.2. - Regular from beta
1.2.3-beta.5: use1.2.3. - Regular from stable: inspect relevant net changes and choose patch, minor, or major by Semantic Versioning.
06. Skip no-op releases - For regular releases, if a target has no includedFiles and no dependency-range cascade, report that there are no relevant release changes and do not bump it. 07. Cascade dependents - Use dependencyEdges to find workspace packages whose dependencies or peerDependencies point at bumped packages. Check ranges with a structured semver parser or package manager API when available, not ad hoc string comparison. If the new version is outside the declared range, update the range and add the dependent to the release plan. Treat dependency range widening as patch by default; treat peer dependency major changes as major unless the user confirms otherwise. 08. Confirm inferred versions - For non-dry-run regular releases without explicit versions, ask the user to confirm inferred versions. For multi-package releases, include requested packages and cascaded dependents in the same release-plan confirmation when the agent UI allows it. 09. Preview dry runs - For --dry-run, print the package order, current versions, planned versions, changelog/tag/commit actions, dependency range updates, and skipped files. Stop before edits. 10. Write changelogs - For regular releases only, read references/common-changelog.md after the final package set is known. Use each target's includedFiles to bound diff inspection against its previousTags[package].tag. Include only production-impacting changes; for package.json, include dependencies and peerDependencies changes only, not devDependencies. 11. Edit release files - Update each target package's CHANGELOG.md and package.json. For beta releases, skip CHANGELOG.md. Update any cascaded dependent ranges before committing the dependent release. 12. Format once - After all release edits, run formatting once. If a justfile exists, inspect just --list and prefer the narrowest relevant write recipe; use broad recipes such as just full-write only when no narrower established recipe covers the touched files. Without a suitable recipe, use the repo's established formatter commands or leave formatting unchanged. 13. Commit and tag in dependency order - Process dependencies before dependents. Use one commit and one annotated tag per package:
- Single-package commit:
docs: release <version> - Monorepo commit:
docs: release <package> <version> - Single-package tag:
v<version>unless existing tags use bare semver. - Monorepo tag: follow existing tag patterns from
previousTags; default to<package-dir>@<version>.
Changelog Rules
Regular releases must generate entries in CHANGELOG.md following references/common-changelog.md.
- Use categories in this order:
Changed,Added,Removed,Fixed. - Start every entry with a present-tense imperative verb.
- Reference PRs when available; fall back to commit links.
- Merge related commits into one user-facing entry.
- Exclude tests, CI/CD, dev tooling, style-only churn, and
devDependencies. - If
package.jsonhas afilesfield, include only changes under those package files/directories, plus production dependency changes inpackage.json.
Beta releases do not update changelogs.
Script Reference
| Script | Purpose |
|---|---|
scripts/plan-release.mjs | Read-only release discovery and scoped diff facts |
Planner output fields to use:
packagesandtargets: package identity, directory, name, version,files, dependency names, and peer dependency names.previousTags: per-package previous release tag and tag patterns used.changedFiles,includedFiles,excludedFiles: scoped file lists for changelog and no-op decisions.dependencyEdges: workspace dependency and peer dependency relationships.workingTree: dirty-tree status that must be clean before release edits.needsSelectionanderrors: package-selection or argument problems to resolve before proceeding.
Examples
# Regular release
/bump-release
# Preview without writes
/bump-release --dry-run
# Beta release
/bump-release --beta
# Monorepo package release
/bump-release evm
# Multi-package monorepo release
/bump-release evm evm-safe
# Explicit single-package version
/bump-release 2.0.0
# Explicit beta version
/bump-release 2.0.0-beta.1Version Examples
| Current Version | Release Type | New Version |
|---|---|---|
1.2.3 | Regular | 1.2.4 (patch) |
1.2.3 | Beta | 1.2.4-beta.1 |
1.2.3-beta.1 | Beta | 1.2.3-beta.2 |
1.2.3-beta.5 | Regular | 1.2.3 |
1.2.3 | 2.0.0 | 2.0.0 |
1.2.3 | 2.0.0 + Beta | 2.0.0-beta.1 |
Resources
references/common-changelog.md- Read only for regular releases after the final stable release package set is known.
policy:
allow_implicit_invocation: true
Common Changelog
Stripped-down reference for generating changelogs. Full spec: <https://common-changelog.org/>
Guiding Principles
- Changelogs are for humans.
- Communicate the impact of changes.
- Sort content by importance.
- Skip content that isn't important.
- Link each change to further information.
Format
File Format
Filename must be CHANGELOG.md. Content must be Markdown starting with:
# ChangelogReleases must be sorted latest-first by semver. There must be an entry for every new stable release.
Release
A release starts with a second-level heading:
## VERSION - DATEVERSION is semver-valid (no "v" prefix) matching a git tag (with optional "v" prefix). DATE is YYYY-MM-DD (ISO 8601).
## 1.0.1 - 2019-08-24The version should link to further information. If hosted on GitHub, link to a GitHub release. Use reference-style links to keep raw Markdown readable:
## [1.0.1] - 2019-08-24
### Fixed
- Prevent segmentation fault upon `close()`
[1.0.1]: https://github.com/owner/name/releases/tag/v1.0.1After the heading, a release must contain either:
1. One or more change groups 2. A notice followed by zero or more change groups
Notice
A single-sentence paragraph with Markdown emphasis, used for upgrade guides, status clarification, or first releases:
## [2.0.0] - 2020-07-23
_If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md)._## [1.0.0] - 2019-08-23
_First release._There can only be one notice per release.
Change Group
A change group starts with a third-level heading containing a category:
### <category>Categories must be one of (in this order):
Changed— changes in existing functionalityAdded— new functionalityRemoved— removed functionalityFixed— bug fixes
The heading is followed by an unnumbered Markdown list. Each item is a single line: a change, then references, then optional authors.
- Prevent buffer overflow ([#28](https://github.com/owner/name/pull/28))Sort the list: breaking changes first, then by importance, then latest-first.
Change
Write using imperative mood. Must start with a present-tense verb (e.g. Add, Refactor, Bump, Document, Fix, Deprecate).
Each change must be self-describing, as if no category heading exists. Instead of:
### Added
- Support of CentOS
- `write()` methodWrite:
### Added
- Support CentOS
- Add `write()` methodReferences
Each change should reference a PR when available; fall back to a commit hash only if no PR exists. References are written after the change on the same line, wrapped in parentheses. Each reference must be a Markdown link.
Pull requests (preferred):
([#194](https://github.com/owner/name/issues/194))Commits (fallback):
([`53bd922`](https://github.com/owner/name/commit/53bd922))When there are more than two references, only include the best starting point.
Authors
Author names are optional and should generally be omitted. Only include them in multi-contributor projects where attribution matters.
Prefixes
Breaking changes must be prefixed in bold: **Breaking:** and listed before other changes per category:
### Changed
- **Breaking:** emit `close` event after `end`
- Refactor `sort()` internals to improve performance
### Removed
- **Breaking:** drop support of Node.js 8For subsystem prefixes: **<subsystem> (breaking):** :
- **Installer (breaking):** enable silent mode by default
- **UI:** tune button colors for accessibilityWriting
Remove Noise
Exclude maintenance changes not interesting to consumers:
- Dotfile changes (
.gitignore,.github,.gitlab, etc.) - Changes to development-only dependencies
- Minor code style changes
- Formatting changes in documentation
Do not exclude:
- Refactorings (may have unintentional side effects)
- Changes to supported runtime environments
- Code style changes that use new language features
- New documentation for previously undocumented features
Rephrase Changes
Align terminology across contributors. Add details where missing, strip details when irrelevant. Instead of:
- Upgrade json-parser from 2.2.0 to 3.0.1
- Bump `xml-parser`Write:
- Bump `json-parser` from 2.x to 3.x
- Bump `xml-parser` from 6.x to 8.xMerge Related Changes
If a change happened over multiple commits, list them as one:
- Bump `standard` from 14.x to 16.x (a, b)Fixups get absorbed into the original change:
- Support filtering entries by name (a, b)Skip No-Op Changes
If commits between releases negate each other (e.g. one reverts the other), leave them out.
Separate Message and Description
A change should be brief — no more than one line. Long descriptions belong in commits or referenced PRs:
- **Breaking:** bump `yaml-parser` from 4.x to 5.x (`15d5a9e`)Exception — if the commit lacks a description:
- **Breaking:** bump `yaml-parser` from 4.x to 5.x (`15d5a9e`). Removes the `unsafe` option.Promoting a Prerelease
Three approaches:
A. Copy content to release — Merge content from prereleases into the release entry, following the same writing practices. Write as if prereleases don't exist.
B. Skip changelog entry for prerelease — When the prerelease is for internal testing only.
C. Refer to prerelease — Use a notice: _Stable release based on <prerelease version>._
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
const ignoredDirs = new Set([
".git",
".next",
".venv",
"build",
"coverage",
"dist",
"node_modules",
"out",
"target",
"vendor",
]);
const options = parseArgs(process.argv.slice(2));
if (options.help) {
printUsage();
process.exit(0);
}
const cwd = path.resolve(options.cwd);
const argErrors = [];
if (options.version && !isSemver(options.version)) {
argErrors.push(`invalid --version: ${options.version}`);
}
let repoRoot;
try {
repoRoot = git(["rev-parse", "--show-toplevel"], cwd).trim();
} catch {
console.error(`ERROR: not inside a git repository: ${cwd}`);
process.exit(2);
}
const rootPackagePath = path.join(repoRoot, "package.json");
if (!fs.existsSync(rootPackagePath)) {
console.error(`ERROR: no package.json found at repo root: ${repoRoot}`);
process.exit(2);
}
const rootPackage = readJson(rootPackagePath, argErrors);
const hasPnpmWorkspace = fs.existsSync(path.join(repoRoot, "pnpm-workspace.yaml"));
const workspacePatterns = workspaceGlobs(repoRoot, rootPackage);
const isMonorepo = workspacePatterns.length > 0 || hasPnpmWorkspace;
const packageRecords = isMonorepo
? discoverWorkspacePackages(repoRoot, workspacePatterns, argErrors, { preferPnpm: hasPnpmWorkspace })
: [readPackage(repoRoot, repoRoot, argErrors)].filter(Boolean);
const releaseTags = gitTags(repoRoot);
const selectedTargets = resolveTargets({
cwd,
isMonorepo,
packages: packageRecords,
repoRoot,
selectors: options.packages,
errors: argErrors,
});
if (options.version && selectedTargets.length > 1) {
argErrors.push("explicit --version is only valid for a single target package");
}
const output = {
cwd,
repoRoot,
mode: isMonorepo ? "monorepo" : "single-package",
beta: options.beta,
dryRun: options.dryRun,
explicitVersion: options.version ?? null,
needsSelection: isMonorepo && options.packages.length === 0 && selectedTargets.length === 0,
errors: argErrors,
workingTree: readWorkingTree(repoRoot),
workspacePatterns,
packages: packageRecords.map(serializePackage),
targets: selectedTargets.map(serializePackage),
dependencyEdges: dependencyEdges(packageRecords),
previousTags: Object.fromEntries(packageRecords.map((pkg) => [pkg.id, previousTag(pkg, isMonorepo, releaseTags)])),
changedFiles: {},
includedFiles: {},
excludedFiles: {},
};
for (const pkg of packageRecords) {
const tag = output.previousTags[pkg.id]?.tag ?? null;
const changed = changedFiles(repoRoot, pkg, tag);
const classified = classifyFiles(pkg, changed);
output.changedFiles[pkg.id] = changed;
output.includedFiles[pkg.id] = classified.included;
output.excludedFiles[pkg.id] = classified.excluded;
}
console.log(`${JSON.stringify(output, null, 2)}\n`);
process.exit(argErrors.length > 0 ? 64 : 0);
function parseArgs(args) {
const parsed = {
beta: false,
cwd: process.cwd(),
dryRun: false,
help: false,
packages: [],
version: null,
};
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === "--help" || arg === "-h") {
parsed.help = true;
} else if (arg === "--beta") {
parsed.beta = true;
} else if (arg === "--dry-run") {
parsed.dryRun = true;
} else if (arg === "--cwd") {
parsed.cwd = requireValue(args, (i += 1), "--cwd");
} else if (arg.startsWith("--cwd=")) {
parsed.cwd = arg.slice("--cwd=".length);
} else if (arg === "--version") {
parsed.version = requireValue(args, (i += 1), "--version");
} else if (arg.startsWith("--version=")) {
parsed.version = arg.slice("--version=".length);
} else if (arg === "--package") {
parsed.packages.push(requireValue(args, (i += 1), "--package"));
} else if (arg.startsWith("--package=")) {
parsed.packages.push(arg.slice("--package=".length));
} else if (arg.startsWith("-")) {
throwUsage(`unknown option: ${arg}`);
} else if (!parsed.version && isSemver(arg)) {
parsed.version = arg;
} else {
parsed.packages.push(arg);
}
}
return parsed;
}
function requireValue(args, index, flag) {
const value = args[index];
if (!value || value.startsWith("--")) throwUsage(`${flag} requires a value`);
return value;
}
function throwUsage(message) {
console.error(`ERROR: ${message}\n`);
printUsage();
process.exit(64);
}
function printUsage() {
console.error(`Usage: plan-release.mjs [--cwd <repo>] [--beta] [--dry-run] [--version <semver>] [--package <name-or-dir>]...`);
}
function git(args, cwdArg) {
return execFileSync("git", args, { cwd: cwdArg, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
}
function readJson(filePath, errors) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
errors.push(`${path.relative(process.cwd(), filePath)}: ${error.message}`);
return {};
}
}
function workspaceGlobs(root, rootPackageJson) {
const globs = [];
const workspaces = rootPackageJson.workspaces;
if (Array.isArray(workspaces)) {
globs.push(...workspaces);
} else if (workspaces && Array.isArray(workspaces.packages)) {
globs.push(...workspaces.packages);
}
const pnpmWorkspace = path.join(root, "pnpm-workspace.yaml");
if (fs.existsSync(pnpmWorkspace)) {
globs.push(...readPnpmWorkspaceGlobs(pnpmWorkspace));
}
return unique(globs.filter((glob) => typeof glob === "string" && glob));
}
function readPnpmWorkspaceGlobs(filePath) {
const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
const globs = [];
let inPackages = false;
for (const line of lines) {
if (/^packages:\s*$/.test(line)) {
inPackages = true;
continue;
}
if (inPackages && /^\S/.test(line)) break;
const match = inPackages && line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
if (match) globs.push(match[1]);
}
return globs;
}
function discoverWorkspacePackages(root, globs, errors, { preferPnpm = false } = {}) {
const pnpmPackages = preferPnpm ? discoverPnpmWorkspacePackages(root, errors) : null;
if (pnpmPackages?.length) return pnpmPackages;
const packageDirs = findPackageDirs(root)
.filter((dir) => dir !== root)
.filter((dir) => {
if (globs.length === 0) return true;
const rel = slash(path.relative(root, dir));
return matchesWorkspaceGlobs(rel, globs);
})
.sort((a, b) => slash(path.relative(root, a)).localeCompare(slash(path.relative(root, b))));
return packageDirs.map((dir) => readPackage(root, dir, errors)).filter(Boolean);
}
function discoverPnpmWorkspacePackages(root, errors) {
try {
const projects = JSON.parse(
execFileSync("pnpm", ["--dir", root, "list", "-r", "--depth", "-1", "--json"], {
cwd: root,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}),
);
if (!Array.isArray(projects)) return null;
return projects
.map((project) => workspaceProjectDir(root, project.path))
.filter((dir) => dir && dir !== root)
.map((dir) => readPackage(root, dir, errors))
.filter(Boolean)
.sort((a, b) => a.dir.localeCompare(b.dir));
} catch {
return null;
}
}
function workspaceProjectDir(root, projectPath) {
if (!projectPath) return null;
const rootReal = safeRealpath(root);
const projectReal = safeRealpath(projectPath);
const rel = slash(path.relative(rootReal, projectReal));
if (rel === "" || rel.startsWith("../") || rel === "..") return null;
return path.join(root, rel);
}
function safeRealpath(value) {
try {
return fs.realpathSync.native(value);
} catch {
return path.resolve(value);
}
}
function findPackageDirs(root) {
const results = [];
walk(root);
return results;
function walk(dir) {
const base = path.basename(dir);
if (ignoredDirs.has(base)) return;
const packagePath = path.join(dir, "package.json");
if (fs.existsSync(packagePath)) results.push(dir);
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (ignoredDirs.has(entry.name)) continue;
walk(path.join(dir, entry.name));
}
}
}
function readPackage(root, dir, errors) {
const packagePath = path.join(dir, "package.json");
if (!fs.existsSync(packagePath)) return null;
const manifest = readJson(packagePath, errors);
const rel = slash(path.relative(root, dir)) || ".";
const id = rel === "." ? manifest.name || "." : rel;
return {
id,
dir: rel,
absDir: dir,
name: manifest.name ?? null,
version: manifest.version ?? null,
files: Array.isArray(manifest.files) ? manifest.files : null,
dependencies: manifest.dependencies ?? {},
peerDependencies: manifest.peerDependencies ?? {},
};
}
function resolveTargets({ cwd: cwdArg, errors, isMonorepo, packages, repoRoot: root, selectors }) {
if (!isMonorepo) return packages;
if (selectors.length > 0) {
const selected = [];
for (const selector of selectors) {
const matches = packages.filter((pkg) => packageMatches(pkg, selector));
if (matches.length === 0) {
errors.push(`unknown package selector: ${selector}`);
} else if (matches.length > 1) {
errors.push(`ambiguous package selector: ${selector}`);
} else if (!selected.some((pkg) => pkg.id === matches[0].id)) {
selected.push(matches[0]);
}
}
return selected;
}
const matches = packages.filter((pkg) => {
const rel = pkg.dir === "." ? "" : pkg.dir;
const abs = path.join(root, rel);
return cwdArg === abs || cwdArg.startsWith(`${abs}${path.sep}`);
});
return matches.length === 1 ? matches : [];
}
function packageMatches(pkg, selector) {
const normalized = selector.replace(/\/$/, "");
const base = path.basename(pkg.dir);
const unscoped = pkg.name?.startsWith("@") ? pkg.name.split("/").at(-1) : pkg.name;
return normalized === pkg.dir || normalized === base || normalized === pkg.name || normalized === unscoped;
}
function serializePackage(pkg) {
return {
id: pkg.id,
dir: pkg.dir,
name: pkg.name,
version: pkg.version,
files: pkg.files,
dependencyNames: Object.keys(pkg.dependencies).sort(),
peerDependencyNames: Object.keys(pkg.peerDependencies).sort(),
};
}
function dependencyEdges(packages) {
const byName = new Map(packages.filter((pkg) => pkg.name).map((pkg) => [pkg.name, pkg]));
const edges = [];
for (const from of packages) {
for (const [type, deps] of [
["dependencies", from.dependencies],
["peerDependencies", from.peerDependencies],
]) {
for (const [name, range] of Object.entries(deps)) {
const to = byName.get(name);
if (to) edges.push({ from: from.id, to: to.id, type, name, range });
}
}
}
return edges.sort((a, b) => `${a.from}:${a.to}:${a.type}`.localeCompare(`${b.from}:${b.to}:${b.type}`));
}
function previousTag(pkg, isMonorepoArg, allTags) {
const candidates = tagPatterns(pkg, isMonorepoArg);
const parsed = candidateTags(pkg, isMonorepoArg, allTags)
.map((tag) => ({ tag, version: versionFromTag(tag, isMonorepoArg) }))
.filter((entry) => entry.version)
.sort((a, b) => compareSemverDesc(a.version, b.version));
return {
tag: parsed[0]?.tag ?? null,
version: parsed[0]?.version ?? null,
patterns: candidates,
};
}
function tagPatterns(pkg, isMonorepoArg) {
if (!isMonorepoArg) return ["v[0-9]*.[0-9]*.[0-9]*", "[0-9]*.[0-9]*.[0-9]*"];
return packageTagNames(pkg).flatMap((name) => [`${name}@[0-9]*.[0-9]*.[0-9]*`, `${name}@v[0-9]*.[0-9]*.[0-9]*`]);
}
function gitTags(root) {
try {
return git(["tag", "--list", "--sort=-creatordate"], root)
.split(/\r?\n/)
.filter(Boolean);
} catch {
return [];
}
}
function candidateTags(pkg, isMonorepoArg, allTags) {
if (!isMonorepoArg) {
return unique([
...allTags.filter((tag) => tag.startsWith("v") && stripLeadingV(tag)),
...allTags.filter((tag) => !tag.startsWith("v") && stripLeadingV(tag)),
]);
}
const matches = [];
for (const name of packageTagNames(pkg)) {
for (const hasLeadingV of [false, true]) {
matches.push(
...allTags.filter((tag) => {
const parsed = parsePackageTag(tag);
return parsed?.name === name && parsed.hasLeadingV === hasLeadingV;
}),
);
}
}
return unique(matches);
}
function packageTagNames(pkg) {
const names = new Set([pkg.dir, path.basename(pkg.dir)]);
if (pkg.name) {
names.add(pkg.name);
if (pkg.name.startsWith("@")) names.add(pkg.name.split("/").at(-1));
}
return [...names].filter(Boolean);
}
function parsePackageTag(tag) {
const at = tag.lastIndexOf("@");
if (at <= 0) return null;
const name = tag.slice(0, at);
const suffix = tag.slice(at + 1);
const hasLeadingV = suffix.startsWith("v");
const version = stripLeadingV(suffix);
return version ? { hasLeadingV, name, version } : null;
}
function versionFromTag(tag, isMonorepoArg) {
if (!isMonorepoArg) return stripLeadingV(tag);
return parsePackageTag(tag)?.version ?? null;
}
function changedFiles(root, pkg, tag) {
const args = tag ? ["diff", "--name-only", `${tag}..HEAD`, "--"] : ["ls-files", "--"];
if (pkg.dir !== ".") args.push(pkg.dir);
return git(args, root)
.split(/\r?\n/)
.filter(Boolean)
.sort();
}
function classifyFiles(pkg, files) {
const included = [];
const excluded = [];
for (const file of files) {
const packageRel = pkg.dir === "." ? file : slash(path.relative(pkg.dir, file));
const filesMatch = pkg.files ? matchesFilesField(packageRel, pkg.files) || packageRel === "package.json" : true;
const production = pkg.files ? filesMatch : isProductionFile(file, packageRel);
const entry = { path: file, reason: production ? "included" : exclusionReason(file, packageRel) };
(production ? included : excluded).push(entry);
}
return { included, excluded };
}
function matchesFilesField(packageRel, filesField) {
const rel = packageRel.replace(/^\.\//, "");
return filesField.some((entry) => {
const normalized = normalizeGlob(entry).replace(/\/$/, "");
if (!hasGlob(normalized)) return rel === normalized || rel.startsWith(`${normalized}/`);
return matchGlob(rel, normalized);
});
}
function isProductionFile(repoRel, packageRel) {
const rel = slash(repoRel);
const local = slash(packageRel);
const base = path.basename(local);
if (/^(\.github|\.gitlab|\.circleci|\.husky)\//.test(rel)) return false;
if (/(^|\/)(__tests__|tests?|fixtures?|mocks?)\//i.test(local)) return false;
if (/\.(test|spec|bench|fixture)\.[cm]?[jt]sx?$/i.test(base)) return false;
if (/^(eslint|prettier|biome|vitest|jest|commitlint|lint-staged|lefthook|husky)\.config\./.test(base)) return false;
if (/^(\.eslintrc|\.prettierrc|\.lintstagedrc|\.npmrc|\.node-version|\.nvmrc)$/.test(base)) return false;
if (/^(justfile|Makefile|Dockerfile)$/.test(base)) return false;
if (/^(pnpm-lock\.yaml|bun\.lockb?|package-lock\.json|yarn\.lock)$/.test(base)) return false;
return true;
}
function exclusionReason(repoRel, packageRel) {
const rel = slash(repoRel);
const local = slash(packageRel);
const base = path.basename(local);
if (/^(\.github|\.gitlab|\.circleci|\.husky)\//.test(rel)) return "ci";
if (/(^|\/)(__tests__|tests?|fixtures?|mocks?)\//i.test(local) || /\.(test|spec|bench|fixture)\.[cm]?[jt]sx?$/i.test(base)) {
return "test";
}
return "tooling";
}
function readWorkingTree(root) {
const status = git(["status", "--porcelain=v1"], root)
.split(/\r?\n/)
.filter(Boolean);
return { clean: status.length === 0, status };
}
function matchesWorkspaceGlobs(rel, globs) {
const positive = [];
const negative = [];
for (const glob of globs) {
const negated = glob.startsWith("!");
const normalized = normalizeGlob(negated ? glob.slice(1) : glob).replace(/\/$/, "");
if (!normalized) continue;
(negated ? negative : positive).push(normalized);
}
const included = positive.length === 0 || positive.some((glob) => matchWorkspaceGlob(rel, glob));
return included && !negative.some((glob) => matchWorkspaceGlob(rel, glob));
}
function matchWorkspaceGlob(rel, glob) {
if (!hasGlob(glob)) return rel === glob || rel.startsWith(`${glob}/`);
return matchGlob(rel, glob);
}
function normalizeGlob(glob) {
return slash(glob).replace(/^\.\//, "").replace(/\/package\.json$/, "");
}
function matchGlob(value, glob) {
return globToRegex(glob).test(value);
}
function globToRegex(glob) {
let source = "^";
for (let i = 0; i < glob.length; i += 1) {
const char = glob[i];
const next = glob[i + 1];
if (char === "*" && next === "*") {
if (glob[i + 2] === "/") {
source += "(?:.*/)?";
i += 2;
} else {
source += ".*";
i += 1;
}
} else if (char === "*") {
source += "[^/]*";
} else if (char === "?") {
source += "[^/]";
} else {
source += escapeRegex(char);
}
}
source += "$";
return new RegExp(source);
}
function hasGlob(value) {
return /[*?[\]{}]/.test(value);
}
function escapeRegex(value) {
return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
}
function slash(value) {
return value.split(path.sep).join("/");
}
function unique(values) {
return [...new Set(values)];
}
function isSemver(value) {
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/.test(value);
}
function stripLeadingV(value) {
const version = value.startsWith("v") ? value.slice(1) : value;
return isSemver(version) ? version : null;
}
function compareSemverDesc(a, b) {
return -compareSemver(a, b);
}
function compareSemver(a, b) {
const left = parseSemver(a);
const right = parseSemver(b);
for (const key of ["major", "minor", "patch"]) {
if (left[key] !== right[key]) return left[key] - right[key];
}
if (left.pre.length === 0 && right.pre.length === 0) return 0;
if (left.pre.length === 0) return 1;
if (right.pre.length === 0) return -1;
const length = Math.max(left.pre.length, right.pre.length);
for (let i = 0; i < length; i += 1) {
const l = left.pre[i];
const r = right.pre[i];
if (l === undefined) return -1;
if (r === undefined) return 1;
if (l === r) continue;
const lNum = /^\d+$/.test(l);
const rNum = /^\d+$/.test(r);
if (lNum && rNum) return Number(l) - Number(r);
if (lNum) return -1;
if (rNum) return 1;
return l.localeCompare(r);
}
return 0;
}
function parseSemver(value) {
const [withoutBuild] = value.split("+");
const [core, pre = ""] = withoutBuild.split("-");
const [major, minor, patch] = core.split(".").map(Number);
return { major, minor, patch, pre: pre ? pre.split(".") : [] };
}
Related skills
How it compares
Choose bump-release for Common Changelog-compliant human notes; use conventional-changelog tooling when you need fully automated commit-parser pipelines.
FAQ
What is bump-release?
Use for release versioning: bump/cut/tag a release, bump version, create a release, changelog updates, or version tagging.
When should I use bump-release?
Use for release versioning: bump/cut/tag a release, bump version, create a release, changelog updates, or version tagging.
Is bump-release safe to install?
Review the Security Audits panel on this page before production use.