Skip to content

feat(config)!: unify per-ecosystem enablement — detection enables, config opts out - #563

Merged
goosewobbler merged 1 commit into
mainfrom
feat/unify-ecosystem-enablement
Jul 19, 2026
Merged

goosewobbler merged 1 commit into
mainfrom
feat/unify-ecosystem-enablement

Conversation

@goosewobbler

@goosewobbler goosewobbler commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

What

Make ecosystem enablement symmetric: detection enables, config opts out. Every ecosystem ReleaseKit detects is versioned and — where publishing is configured and authenticated — published by default. Explicit config is only ever an opt-out.

npm crates.io pub.dev
version.*.enabled true (new toggle) true true
publish.*.enabled true true (was false) true (was false)

Changes

  • publish.cargo.enabled / publish.pub.enabled default false → true.
  • Add version.npm.enabled (default true) so package.json handling is opt-out-able like Cargo.toml/pubspec.yaml, wired into every version-stage package.json write (packageProcessor, sync/single strategies, group + ungrouped-member strategies, and the root lockstep bump).
  • Reframe the enabled .describe() text as opt-outs; regenerate releasekit.schema.json + docs/configuration.md (schema:check passes).
  • Migration note in docs/migration.md.

Two independent opt-outs per ecosystem are kept deliberately:

  • version.<eco>.enabled: false → don't touch this ecosystem's manifests at all.
  • publish.<eco>.enabled: false → version and tag, but don't publish.

Why it's safe

Default-on relies on the pipeline's existing structural fail-safes (publish/src/registry/dispatcher.ts): an enabled registry that discovers zero targets no-ops, and publishing without credentials fails/skips at the auth check. This repo (npm-only) is unaffected — the cargo/pub flip no-ops with no Rust/Dart packages.

Tests

  • Schema: publish defaults on for npm/cargo/pub; version.npm.enabled defaults true and accepts an opt-out.
  • Behavior: version.npm.enabled: false leaves package.json untouched while Cargo.toml is still versioned.

Breaking change

crates.io and pub.dev publishing now default on for detected packages. Set publish.cargo.enabled / publish.pub.enabled to false to opt out (version and tag only).

Closes #554

Greptile Summary

This PR makes ecosystem enablement symmetric across npm, Cargo, and pub.dev: every detected ecosystem is versioned and published by default, with explicit config only ever serving as an opt-out. It adds version.npm.enabled (default true) to make package.json handling opt-out-able like Cargo.toml/pubspec.yaml, flips publish.cargo.enabled and publish.pub.enabled from false to true, and wires the new npm gate into every version-stage code path.

  • version.npm.enabled gate added to sync strategy (root + per-package loop), single strategy, async/packageProcessor, group strategy (grouped members + ungrouped packages) — all paths consistently check config.npm?.enabled !== false before writing package.json.
  • Three previously-reported bugs fixed: processedPaths.add(rootPkgPath) is now unconditional (single-package dedup), root Cargo.toml update runs outside the npm guard (no silent skip), and hybrid-package Cargo.toml changes are tracked in updatedPackages when npm is disabled (no abandoned writes).
  • Schema, JSON schema, and docs regenerated and consistent; two targeted test cases cover the dedup and abandoned-write scenarios directly.

Confidence Score: 5/5

Safe to merge — the three logic bugs called out in prior review rounds are all correctly addressed, and the new npm-gate is applied consistently across every version-stage code path.

The sync-strategy restructuring (unconditional processedPaths.add, root Cargo outside the npm gate, hybrid-package Cargo tracking) is well-reasoned and covered by two targeted new tests. Schema, docs, and type changes are internally consistent. The only gap is a documentation omission rather than a logic defect.

No files require special attention; the documentation gap (missing migration note) is the only remaining item.

Important Files Changed

Filename Overview
packages/version/src/core/versionStrategies.ts Sync strategy correctly restructured: processedPaths.add(rootPkgPath) is now unconditional, root Cargo.toml runs outside the npm gate, and hybrid-package Cargo.toml tracking fires when either package.json is absent or npm is disabled — all three previously-reported bugs are fixed.
packages/config/src/schema.ts Adds VersionNpmConfigSchema (enabled: true default), wires it into VersionConfigSchema.npm, and flips CargoPublishConfigSchema/PubPublishConfigSchema enabled defaults from false to true — consistent with the detection-enables model.
packages/version/src/core/groupStrategy.ts Both releaseGroup members and ungrouped packages now correctly gate package.json writes on config.npm?.enabled !== false; Cargo.toml handling is unchanged and independent of the npm gate.
packages/version/src/package/packageProcessor.ts Adds npmEnabled check before writing package.json; Cargo.toml and pubspec.yaml handling remain independent. updatedPackagesInfo is still pushed unconditionally after version calculation, so the async strategy commit path is unaffected when npm is disabled.
packages/version/src/types.ts Adds npm?: { enabled?: boolean } to the Config interface and threads it through toVersionConfig, correctly matching the cargo/pub pattern already established.
packages/version/test/unit/core/versionStrategies.spec.ts Two new tests directly cover the previously-reported bugs: one for hybrid monorepo Cargo.toml not being abandoned, and one for single-package repo dedup (Cargo.toml versioned exactly once). Both assert tagging proceeds correctly.
packages/config/test/unit/schema.spec.ts Tests verify version.npm.enabled defaults to true, accepts opt-out false, and that publish defaults for cargo and pub are now true. Clean.
docs/configuration.md Adds ### version.npm subsection (parallel to ### version.cargo) and updates publish enabled defaults and descriptions to reflect the opt-out framing.
releasekit.schema.json Generated schema correctly reflects the new version.npm object, updated cargo/pub publish defaults (false to true), and refreshed descriptions throughout. Consistent with schema.ts.
scripts/generate-config-docs.ts Adds a renderVersion branch for version.npm parallel to the existing version.cargo branch, ensuring the docs generator produces the new subsection.
packages/version/test/unit/package/packageProcessor.spec.ts New test covers hybrid package processor behavior when npm is disabled: package.json is skipped and Cargo.toml is still updated, verifying the two ecosystems are independent.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Version stage triggered] --> B{packages.root defined?}
    B -->|yes| C{npm enabled?\nconfig.npm?.enabled !== false}
    B -->|no| G
    C -->|yes + package.json exists| D[updatePackageVersion root package.json\nupdatedPackages.push root]
    C -->|no| E[skip package.json\nlog debug]
    D --> F[processedPaths.add rootPkgPath\nunconditional]
    E --> F
    F --> G[updateCargoFiles root dir\nindependent of npm gate]
    G --> H{rootCargoFiles.length > 0\nand root not yet tracked?}
    H -->|yes| I[updatedPackages.push root]
    H -->|no| J
    I --> J[for each pkg in packages]
    J --> K{processedPaths.has\npkg package.json path?}
    K -->|yes - single-pkg dedup| L[continue - skip]
    K -->|no| M{npm enabled?}
    M -->|yes + file exists| N[updatePackageVersion pkg package.json\nupdatedPackages.push pkg.name\nprocessedPaths.add]
    M -->|no| O[skip package.json\nlog debug]
    N --> P[updateCargoFiles pkg dir]
    O --> P
    P --> Q{no package.json OR npm disabled\nAND pkgCargoFiles.length > 0?}
    Q -->|yes| R[updatedPackages.push pkg.name]
    Q -->|no| S
    R --> S{more packages?}
    S -->|yes| J
    S -->|no| T{updatedPackages empty?}
    T -->|yes| U[early return - no packages updated]
    T -->|no| V[proceed to tag + commit]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Version stage triggered] --> B{packages.root defined?}
    B -->|yes| C{npm enabled?\nconfig.npm?.enabled !== false}
    B -->|no| G
    C -->|yes + package.json exists| D[updatePackageVersion root package.json\nupdatedPackages.push root]
    C -->|no| E[skip package.json\nlog debug]
    D --> F[processedPaths.add rootPkgPath\nunconditional]
    E --> F
    F --> G[updateCargoFiles root dir\nindependent of npm gate]
    G --> H{rootCargoFiles.length > 0\nand root not yet tracked?}
    H -->|yes| I[updatedPackages.push root]
    H -->|no| J
    I --> J[for each pkg in packages]
    J --> K{processedPaths.has\npkg package.json path?}
    K -->|yes - single-pkg dedup| L[continue - skip]
    K -->|no| M{npm enabled?}
    M -->|yes + file exists| N[updatePackageVersion pkg package.json\nupdatedPackages.push pkg.name\nprocessedPaths.add]
    M -->|no| O[skip package.json\nlog debug]
    N --> P[updateCargoFiles pkg dir]
    O --> P
    P --> Q{no package.json OR npm disabled\nAND pkgCargoFiles.length > 0?}
    Q -->|yes| R[updatedPackages.push pkg.name]
    Q -->|no| S
    R --> S{more packages?}
    S -->|yes| J
    S -->|no| T{updatedPackages empty?}
    T -->|yes| U[early return - no packages updated]
    T -->|no| V[proceed to tag + commit]
Loading

Comments Outside Diff (3)

  1. packages/version/src/core/versionStrategies.ts, line 226-240 (link)

    P1 Root Cargo.toml skipped when npm versioning is disabled

    The updateCargoFiles call for the workspace root is nested inside the npm-enabled guard, so setting version.npm.enabled: false on a hybrid monorepo (npm + Rust, sync mode) silently skips the root Cargo.toml update entirely. Compare the per-package loop directly below: there, updateCargoFiles runs after the npm block unconditionally. The fix is to move the root updateCargoFiles call (and its files.push) outside the npm gate, matching the pattern used for every workspace package in the same function.

    Fix in Claude Code Fix in Cursor

  2. packages/version/src/core/versionStrategies.ts, line 277-293 (link)

    P1 Cargo changes abandoned on early exit when npm is disabled

    When version.npm.enabled: false on a sync-mode hybrid monorepo (packages have both package.json and Cargo.toml), pkgCargoFiles are written to disk and pushed into files, but updatedPackages never receives the package name — the npm path skips it, and the cargo fallback at line 282 only fires when !fs.existsSync(packageJsonPath) (i.e. pure-Rust packages without a package.json). With updatedPackages empty, the return at line 292 fires and the already-written Cargo.toml changes are never committed, leaving the working tree dirty.

    Fix: extend the cargo tracking to also cover hybrid packages when npm is disabled:

    if ((!fs.existsSync(packageJsonPath) || config.npm?.enabled === false) && pkgCargoFiles.length > 0) {
      updatedPackages.push(pkg.packageJson.name);
    }
    

    Fix in Claude Code Fix in Cursor

  3. packages/version/src/core/versionStrategies.ts, line 258-261 (link)

    P1 processedPaths dedup broken for single-package repos when npm is disabled

    processedPaths is populated only when npm is enabled and a package.json is written (line 275). When version.npm.enabled: false, the set stays empty, so processedPaths.has(packageJsonPath) is always false and the continue guard never fires.

    For a single-package repo configured with sync: true — where packages.root === packages.packages[0].dir — updateCargoFiles is called twice: once unconditionally for the root (line 240) and again in the loop without being short-circuited. The result is the root Cargo.toml path appearing twice in files and updatedPackages being inflated by one. The comment on line 221 (// Track processed paths to avoid duplicates in single-package repos) acknowledges this invariant, but it no longer holds when npm versioning is opted out.

    The simplest fix is to populate processedPaths with packageJsonPath in the npm-disabled branch (line 265) so the loop still skips the root-as-package path, even though no package.json was written.

    Fix in Claude Code Fix in Cursor

Reviews (8): Last reviewed commit: "feat(config)!: unify per-ecosystem enabl..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Standing release PR: #555 · v0.40.1 queued (4 packages) · open 3h 29m · ✅ ready to merge

Release Preview — v0.40.0 → v0.41.0

Note: Labels on this PR are advisory in standing-pr mode. Bumps come from conventional commits in the standing PR; override by editing labels on the standing PR itself. Add release:immediate to bypass the standing PR and release this PR directly.

These changes will be added to the release PR (#555) when merged:

Changelog

monorepo 0.40.0 → 0.41.0

Added

  • BREAKING unify per-ecosystem enablement — detection enables, config opts out (config)

All packages → 0.41.0 (sync versioning)

  • @releasekit/version
  • @releasekit/notes
  • @releasekit/publish
  • @releasekit/release

After merge — predicted release

Approximate. The standing PR rebuilds against main at merge time; if other commits land first, the prediction may shift.

Package Standing PR This PR After merge
monorepo 0.40.1 0.41.0 0.41.0 ⚠ escalated from 0.40.1

Updated automatically by ReleaseKit

Comment thread docs/configuration.md
@goosewobbler
goosewobbler force-pushed the feat/unify-ecosystem-enablement branch 4 times, most recently from 61f1373 to 037e4db Compare July 19, 2026 16:45
@goosewobbler goosewobbler changed the title feat(config): unify per-ecosystem enablement — detection enables, config opts out feat(config)!: unify per-ecosystem enablement — detection enables, config opts out Jul 19, 2026
@goosewobbler

Copy link
Copy Markdown
Owner Author

Both P1s addressed in the latest push:

  • Root Cargo.toml skipped when npm disabled — moved the root updateCargoFiles call out of the config.npm?.enabled !== false gate so the root Cargo.toml is versioned independently of npm handling, matching the per-package loop.
  • Hybrid cargo abandoned on early exit — the per-package cargo tracking now also fires when npm is disabled ((!fs.existsSync(packageJsonPath) || config.npm?.enabled === false) && pkgCargoFiles.length > 0), so updatedPackages isn't empty and the run no longer early-returns and abandons the written Cargo.toml.

Verified the single/async (packageProcessor) and group strategies track members independently of which manifest was written, so they don't have the same class of bug. Added a sync-strategy regression test for the npm-disabled hybrid case.

@goosewobbler
goosewobbler force-pushed the feat/unify-ecosystem-enablement branch from 037e4db to 65e6237 Compare July 19, 2026 16:53
Comment thread packages/version/src/core/versionStrategies.ts
@goosewobbler
goosewobbler force-pushed the feat/unify-ecosystem-enablement branch from 65e6237 to 046a069 Compare July 19, 2026 17:10
@goosewobbler

Copy link
Copy Markdown
Owner Author

Addressed the follow-up P1 (processedPaths dedup): moved processedPaths.add(rootPkgPath) out of the npm-enabled guard so the root path is recorded unconditionally. In a single-package repo where packages.root === packages[0].dir, the per-package loop now skips it even when version.npm.enabled: false, so its Cargo.toml is versioned once, not twice. Added a single-package regression test asserting exactly one cargo write for the shared dir.

…nfig opts out

Every detected ecosystem is versioned and, where publishing is configured and
authenticated, published by default; explicit config is only ever an opt-out.

- publish.cargo.enabled / publish.pub.enabled now default true (was false)
- add version.npm.enabled (default true) so package.json handling is opt-out-able
  like Cargo.toml/pubspec.yaml; wired into every version-stage package.json write
- keep Cargo.toml (root and hybrid packages) versioned when npm handling is
  disabled — the npm gate must not skip or abandon cargo updates
- reframe the enabled describe() text as opt-outs; regenerate schema + config docs

Default-on is safe: an enabled registry that finds zero targets no-ops, and
publishing without credentials fails/skips at the auth check.

Refs #554

BREAKING CHANGE: crates.io and pub.dev publishing now default on for detected
packages. Set publish.cargo.enabled / publish.pub.enabled to false to opt out
(version and tag only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011i1KvoyxATU3bz5V5Sf1Xu
@goosewobbler
goosewobbler force-pushed the feat/unify-ecosystem-enablement branch from 046a069 to 56166dd Compare July 19, 2026 17:35
@goosewobbler

Copy link
Copy Markdown
Owner Author

Addressed the single-package abandonment P1: the root block now records 'root' in updatedPackages when only the root Cargo.toml was versioned (npm disabled, or no root package.json), not just when package.json is written. So a single-package repo (packages.root === packages[0].dir) with npm disabled no longer early-returns empty and abandon the written Cargo.toml — the shared tag is created and the file committed. Extended the single-package regression test with an addTag assertion (verifies the run completes, not just the write count).

@goosewobbler
goosewobbler merged commit 44ba43b into main Jul 19, 2026
27 checks passed
@goosewobbler
goosewobbler deleted the feat/unify-ecosystem-enablement branch July 19, 2026 17:47
@github-actions github-actions Bot mentioned this pull request Jul 19, 2026
@github-actions github-actions Bot mentioned this pull request Jul 19, 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.

Unify per-ecosystem enablement: detection enables, config opts out (version + publish)

1 participant