feat!: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx) - #153
Open
theoephraim wants to merge 4 commits into
Open
feat!: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx)#153theoephraim wants to merge 4 commits into
theoephraim wants to merge 4 commits into
Conversation
…ilt-in) Packages can now publish to multiple targets via per-package publishTargets config and a root targets map (type defaults + named reusable instances). Targets execute per-package with per-target retry state in GitHub release metadata, shared artifacts (one .vsix -> marketplace + Open VSX), and a registry-level pre-publish idempotency guard. JSR publishing (publish-time jsr.json version sync, claim-first bootstrap detection) is modeled on Drake Costa's (@Saeris) setup in mirrordown. Legacy publishCommand/skipNpmPublish fields keep working via back-compat mapping in the resolver.
Python packages ride bumpy's package.json spine via a private stub package; the target syncs [project].version into pyproject.toml at publish time, builds with uv into an isolated per-version dist dir, and uploads via uv publish (PyPI trusted publishing / UV_PUBLISH_TOKEN). PyPI project name comes from pyproject.toml. Prereleases/snapshots are skipped (PEP 440 doesn't cover semver suffixes).
- prepare() lifecycle hook — publish-time version syncs (jsr.json, pyproject.toml) get a formal home instead of living in publish/buildArtifact - broken target config no longer breaks read-only commands: discovery records targetsError and publish flows refuse loudly instead - structured TargetOutcome.skipKind replaces magic reason strings coupled across pipeline and publish command - npmEffectiveRegistry() is the single registry fallback chain (options -> bumpy registry -> publishConfig.registry) for publish args, existence checks, prerelease counters, labels, and URLs - refusesPrivatePackages capability flag replaces hardcoded npm-type check in the resolver - jsr.json version sync uses the formatting-preserving updateJsonFields - per-target published checks run in parallel; shared stringArrayOption/ stringOption helpers de-triplicate option coercion
|
The changes in this PR will be included in the next version bump.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What
Packages can now publish to multiple targets at once. Each target is an instance of a plugin implementing one interface (
src/core/targets/); built-ins:npm,jsr,pypi,vscode-marketplace,open-vsx,github-release-assets,docker,homebrew, andcustom(shell-command escape hatch).Breaking (major bump):
publishCommand/checkPublished/skipNpmPublishare removed — bumpy fails with the migration when it sees them. Roottargetsentries no longer inherit from each other. A package's ownpackage.jsonmay only reference targets by name.State model
Three sources of "is this version out", with fixed precedence:
checkPublished) — truth whenever it can answer. Asked before every publish, so a lost draft never causes a duplicate publish, and how astagedtarget is promoted once approved.name@version— how packages with no queryable target are tracked.The tag marks the commit artifacts shipped from: it follows HEAD across failed attempts and freezes the first time anything ships (success or staged). The draft is finalized once every target is live. The pipeline only publishes; tagging/finalizing is orchestrated from its outcomes in one place.
Design highlights
npm succeeded + Open VSX failed→ the retry re-runs only Open VSX.Tblocks dependents onTonly (recorded as failed, retried next run) —app@jsrnever goes out referencing alib@jsrthat didn't land.npmStaged):publish()may report{ status: 'staged', ref }. The target is recorded 🟡 staged with the npm stage id, the draft stays a draft (no dead link, no prematurerelease: published), and the next publish run sees it live and finalizes. Snapshots never stage. Generic — any registry with an approval queue fits the same hook. (feat: mark staged publishes as staged in GH release +publish finalize#147'spublish finalizecommand becomes a thin wrapper over this reconciliation.)release-phase targets constitute it (npm, marketplaces,github-release-assets) — the draft is held until they're done, then published.post-release-phase targets consume it and run only once it's public, because a draft's assets aren't downloadable (a Homebrew formula'surls, a Dockerfile'scurlof the tarball).homebrew/dockerdefault to post-release; any target can set"phase". A package whose release-phase targets didn't all succeed holds its post-release targets for the next run. This is the split varlock'srelease.yamldoes by hand today withrelease-binaries→release-dockerjobs.github-release-assets(uploads to the draft, sorelease: publishedfires only once binaries are attached),docker(buildx --push,:version+:latest+ dist-tag),homebrew(formula template with{{sha256 <asset>}}, committed + tagged + pushed to the tap; token via env, never argv)..vsixuploads to both marketplaces, byte-identical.prepare()):jsr.json/pyproject.tomlversions sync from package.json at publish time — commit them as0.0.0and forget them. JSR identity comes fromjsr.json'sname(JSR scopes are a separate namespace).skipped; a package none of whose targets can take a release kind is dropped from the plan before any draft is opened."private": trueonly means "not npm". A private package that declarespublishTargets(an extension, a PyPI stub, a CLI shipped as release assets) is versioned and published regardless ofprivatePackages.version. (Surfaced by the testbed: none of its private packages were being planned.)checkPublished(npm info,docker manifest inspect,gh release view,gh api,vsce/ovsx) runs with a timeout and reports "unknown" on expiry, so a credential helper waiting for a prompt can't hang a release. (Surfaced by the testbed:docker manifest inspectblocked on the local credential helper.)package.jsonexecutes or steers a publish — only name references.buildCommandand inline target definitions there requireallowCustomCommands. The root config is trusted.End-to-end testbed
dmno-dev/bumpy-testbed(scaffolded alongside this PR) cuts a real patch release of one package per target shape on every run — npm+JSR, npm staged, TestPyPI, a CLI through release-assets → homebrew → docker, and a realvsce package— then verifies from the outside (registries, published release with assets, formula version + sha256,docker run,brew installon macOS). Nightly against@varlock/bumpy@next; a failure-injection mode asserts the held-draft/post-release behavior. Two bumpy bugs above came out of running its config through this branch's dry-run.Validated against real setups
azureCredential: true) and gains per-registry retry.["npm", "jsr"].Credits
JSR publishing behavior is modeled on Drake Costa's setup in mirrordown — thanks Drake! 🙏
Test plan
test/setup.tspreload: no signing/hooks/identity from~/.gitconfig). Fixing that surfaced a Bun quirk worth knowing: children spawned without an explicitenvget the process's initial environment, sowithReleaseToken()-styleprocess.envmutations never reachedgh/gitunder Bun — every spawn inutils/shell.tsnow passesenv: process.envexplicitly.