Skip to content
Open
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
9 changes: 9 additions & 0 deletions .bumpy/publish-targets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@varlock/bumpy': major
---

Publish-target plugin system: packages can now publish to multiple targets at once via per-package `publishTargets` and a root `targets` map of named, reusable instances. Built-in targets: `npm`, `jsr`, `pypi`, `vscode-marketplace`, `open-vsx`, `github-release-assets`, `docker`, `homebrew`, and `custom` (the shell-command escape hatch). Publish state is tracked per target in the GitHub release metadata, and the registry itself is asked before every publish, so a partial failure (npm succeeded, Open VSX errored) retries only what's missing and a lost draft never causes a duplicate publish. A dependency failing on a target blocks dependents on that same target only. Targets publishing the same artifact share one build — a single `.vsix` goes to both the VS Code Marketplace and Open VSX, byte-identical. Marketplace/JSR/PyPI targets sit out snapshots and (where unsupported) prereleases as recorded skips instead of failures. Staged npm publishes (`npmStaged`) are recorded as `staged` and hold the GitHub release as a draft until the version is approved and seen live. Targets run in two phases around the GitHub release: `release`-phase targets constitute it and are published together; `post-release` targets (`homebrew`, `docker`, or anything with `"phase": "post-release"`) consume it and run once it's public, so formulas and Dockerfiles that download release assets work.

**Breaking:** the `publishCommand`, `checkPublished` and `skipNpmPublish` package fields are removed — bumpy fails with the migration when it sees them (`publishCommand`/`checkPublished` → a `custom` target entry in `publishTargets`; `skipNpmPublish: true` → `"publishTargets": []`). Root `targets` entries no longer inherit from each other (`targets.npm` is just the instance named `npm`; the `publish` block remains the defaults for every npm-type instance). A package's own `package.json` may only reference targets by name — inline target definitions (like `buildCommand`) require `allowCustomCommands`.

JSR publishing (publish-time `jsr.json` version sync, claim-first bootstrap detection) is modeled on [Drake Costa's](https://github.com/Saeris) setup in [mirrordown](https://github.com/mirrordown/mirrordown) — thanks Drake!
5 changes: 2 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,8 @@ With `--snapshot <name>`, publish derives a throwaway prerelease version per pen

**How bumpy detects unpublished packages:**

1. Custom `checkPublished` command (if configured per-package — see [`allowCustomCommands`](./configuration.md#custom-commands-and-allowcustomcommands))
2. Git tags (for packages with `skipNpmPublish` or custom `publishCommand`)
3. npm registry query (default)
1. Each publish target that can answer is asked (`npm info` for npm, the JSR/PyPI APIs, a `custom` target's `checkPublished` command) — a package counts as published only when every target says so, so a partial publish re-enters the flow
2. Git tags, for targets that can't answer and for packages with `"publishTargets": []`

## `bumpy check`

Expand Down
188 changes: 168 additions & 20 deletions docs/configuration.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/prereleases.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ How `bumpy publish` (and the publish half of `bumpy ci release`) works on a chan

**Trigger** — in CI, publish fires when the triggering push added files to `.bumpy/<channel>/` (the push event's `before..after` range, falling back to the last commit). That's exactly what merging a release PR does; an ordinary feature merge never touches the channel dir, so it never causes a publish. This requires git history in the checkout — use `fetch-depth: 0`, which the [release workflow](github-actions.md) needs anyway. Running `bumpy publish` manually on the channel branch always publishes the cycle (manual = explicit intent).

**Idempotency & resume** — re-running on the same commit is a no-op: npm records the publishing commit (`gitHead`) in each version's metadata, so bumpy can tell "already published from this exact SHA — skip" apart from "needs the next counter." (Packages publishing outside npm — custom commands, `skipNpmPublish` — use their git tags for the same check.) If a publish fails partway, re-running resumes it package by package; `bumpy publish --filter` remains available as a manual fallback.
**Idempotency & resume** — re-running on the same commit is a no-op: npm records the publishing commit (`gitHead`) in each version's metadata, so bumpy can tell "already published from this exact SHA — skip" apart from "needs the next counter." (Packages without an npm target — custom targets, `publishTargets: []` — use their git tags for the same check.) If a publish fails partway, re-running resumes it package by package; `bumpy publish --filter` remains available as a manual fallback.

**Order of operations** — publish packages topologically, then push tags, then create the GitHub release. Tags are the completion marker, so they go up only after the registry is fully consistent.

Expand Down
3 changes: 3 additions & 0 deletions packages/bumpy/bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

[define]
"__BUMPY_VERSION__" = "'dev'"

[test]
preload = ["./test/setup.ts"]
26 changes: 5 additions & 21 deletions packages/bumpy/src/commands/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { createHash } from 'node:crypto';
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { resolveCommitMessage } from '../core/commit-message.ts';
import type { BumpyConfig, BumpFile, PackageConfig, PackageManager, ReleasePlan, PlannedRelease } from '../types.ts';
import { getPackageTargets } from '../core/targets/registry.ts';
import type { BumpyConfig, BumpFile, PackageManager, ReleasePlan, PlannedRelease, WorkspacePackage } from '../types.ts';

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

Expand Down Expand Up @@ -381,7 +382,7 @@ interface PlanRelease {
bumpFiles: string[];
isDependencyBump: boolean;
isCascadeBump: boolean;
publishTargets: Array<{ type: string }>;
publishTargets: Array<{ type: string; name: string }>;
}

interface PlanOutput {
Expand Down Expand Up @@ -485,7 +486,7 @@ function formatPlanRelease(
isDependencyBump: boolean;
isCascadeBump: boolean;
},
packages: Map<string, { relativeDir: string; private: boolean; bumpy?: PackageConfig }>,
packages: Map<string, WorkspacePackage>,
config: BumpyConfig,
): PlanRelease {
const pkg = packages.get(r.name);
Expand All @@ -498,27 +499,10 @@ function formatPlanRelease(
bumpFiles: r.bumpFiles,
isDependencyBump: r.isDependencyBump,
isCascadeBump: r.isCascadeBump,
publishTargets: getPublishTargets(pkg, config),
publishTargets: pkg ? getPackageTargets(pkg, config).map((t) => ({ type: t.type, name: t.name })) : [],
};
}

function getPublishTargets(
pkg: { private: boolean; bumpy?: PackageConfig } | undefined,
_config: BumpyConfig,
): Array<{ type: string }> {
if (!pkg) return [];
const pkgConfig = pkg.bumpy || {};
if (pkg.private && !pkgConfig.publishCommand) return [];
const targets: Array<{ type: string }> = [];
if (pkgConfig.publishCommand) {
targets.push({ type: 'custom' });
}
if (!pkgConfig.publishCommand && !pkgConfig.skipNpmPublish) {
targets.push({ type: 'npm' });
}
return targets;
}

/** Write a key=value pair to $GITHUB_OUTPUT if available */
function writeGitHubOutput(key: string, value: string): void {
const outputFile = process.env.GITHUB_OUTPUT;
Expand Down
Loading