feat!: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx) - #153
Open
theoephraim wants to merge 5 commits into
Open
feat!: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx)#153theoephraim wants to merge 5 commits into
theoephraim wants to merge 5 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.
|
…tcomes
State precedence is now registry → release metadata → git tag: the pipeline asks
each target's checkPublished before every publish, metadata gates only what the
registry can't answer (prior success, staged re-check, failed retry), and stale
metadata keys are pruned. Tagging moves out of the pipeline into the publish flow:
the tag follows HEAD until anything ships (success or staged), then freezes; push
runs before the failure exit. A dependency failing on a target blocks dependents
on that same target only. publish() may report a staged outcome — npmStaged
records the stage id, holds the draft open, and the next run promotes it once
live (snapshots never stage). Reconciliation lives in core/release-state.ts so a
finalize command can reuse it.
Targets run in two phases around the GitHub release: release-phase targets
constitute it (draft held until done, then published); post-release targets
consume it and run only once it's public, since a draft's assets aren't
downloadable. homebrew/docker default to post-release; any target can set
`phase`. Builds happen in the release pass only.
New targets for CLI distribution:
github-release-assets (uploads to the draft, so `release: published` fires only
once binaries are attached), docker (buildx --push; :version, :latest on stable,
dist-tag on channels/snapshots), homebrew (formula template with {{sha256 asset}}
committed, tagged and pushed to the tap; token via env, never argv).
Tests are hermetic w.r.t. the developer's git config (test/setup.ts preload).
Every spawn in utils/shell.ts passes env: process.env explicitly — under Bun a
child with no env gets the process's initial environment, so credential helpers
that mutate process.env for one call never reached gh/git.
Private packages that declare publishTargets are managed regardless of
privatePackages.version. CLI-based registry probes are bounded by timeouts and
report unknown on expiry, so a credential helper can't hang a release.
BREAKING: publishCommand / checkPublished / skipNpmPublish are removed with a
migration error; targets entries no longer inherit from each other; package.json
may only reference targets by name (buildCommand and inline definitions need
allowCustomCommands). Also: JSR identity from jsr.json name, marketplace
checkPublished matches any version, preflight per distinct instance, no-target
public packages still build and tag.
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.