Skip to content

feat!: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx) - #153

Open
theoephraim wants to merge 4 commits into
mainfrom
claude/goofy-haslett-323848
Open

feat!: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx)#153
theoephraim wants to merge 4 commits into
mainfrom
claude/goofy-haslett-323848

Conversation

@theoephraim

@theoephraim theoephraim commented Jul 31, 2026

Copy link
Copy Markdown
Member

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, custom (shell-command escape hatch), jsr, pypi, vscode-marketplace, open-vsx.

// .bumpy/_config.json
{
  "publish": { "provenance": true },                                    // defaults for every npm-type instance
  "targets": {
    "ghp": { "type": "npm", "registry": "https://npm.pkg.github.com" } // named reusable instance
  },
  "packages": {
    "my-lib": { "publishTargets": ["npm", "ghp", "jsr"] },
    "my-extension": { "publishTargets": ["vscode-marketplace", "open-vsx"] },
    "my-py-tool": { "publishTargets": ["pypi"] } // stub package.json + pyproject.toml
  }
}

Breaking (major bump): publishCommand / checkPublished / skipNpmPublish are removed — bumpy fails with the migration when it sees them. Root targets entries no longer inherit from each other. A package's own package.json may only reference targets by name.

State model

Three sources of "is this version out", with fixed precedence:

  1. The registry (each target's checkPublished) — truth whenever it can answer. Asked before every publish, so a lost draft never causes a duplicate publish, and how a staged target is promoted once approved.
  2. Release metadata — memory for what the registry can't tell us: which targets already succeeded (skip), are staged (re-check), or failed (retry). Stale keys from removed targets are pruned so they can't block finalization.
  3. The git tag 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

  • Per-target resume: npm succeeded + Open VSX failed → the retry re-runs only Open VSX.
  • Per-target ordering: a dependency failing on target T blocks dependents on T only (recorded as failed, retried next run) — app@jsr never goes out referencing a lib@jsr that didn't land.
  • Staged publishing (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 premature release: 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's publish finalize command becomes a thin wrapper over this reconciliation.)
  • Shared artifacts: targets declaring the same artifact kind share one build — a single .vsix uploads to both marketplaces, byte-identical.
  • Publish-time version sync (prepare()): jsr.json / pyproject.toml versions sync from package.json at publish time — commit them as 0.0.0 and forget them. JSR identity comes from jsr.json's name (JSR scopes are a separate namespace).
  • Capabilities: marketplaces (no prereleases), JSR/PyPI (no snapshots) record skipped; a package none of whose targets can take a release kind is dropped from the plan before any draft is opened.
  • Trust model: nothing from a package's package.json executes or steers a publish — only name references. buildCommand and inline target definitions there require allowCustomCommands. The root config is trusted.

Validated against real setups

  • varlock: the vscode extension's script chain collapses to two targets (incl. azureCredential: true) and gains per-registry retry.
  • mirrordown: four hand-rolled publish scripts covered by built-ins; config collapses to ["npm", "jsr"].

Credits

JSR publishing behavior is modeled on Drake Costa's setup in mirrordown — thanks Drake! 🙏

Test plan

  • 473 tests (resolver/trust-gate/migration errors, multi-target pipeline incl. per-target retry, per-target blocking, staged re-check + promotion, registry guards for marketplaces/JSR/PyPI, shared artifacts, preflight per distinct instance)
  • Not yet done: a live end-to-end publish — first real validation will be a varlock release after migrating its config.

…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
@github-actions

Copy link
Copy Markdown

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • @varlock/bumpy 1.18.1 → 1.19.0

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@theoephraim theoephraim changed the title feat: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx) feat!: publish-target plugin system (npm / jsr / pypi / vscode-marketplace / open-vsx) Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant