Skip to content
Merged
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
17 changes: 13 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Versioning configuration.
| `allowFirstBump` | boolean | `false` | Acknowledge applying a bump on a first release with an already-stable manifest. On a first release (no prior tag), `--stable --bump <type>` applies the bump (e.g. 1.0.0 → 2.0.0) rather than graduating, which can silently overshoot the staged first version. By default this is flagged per `mismatchStrategy` (warn, or abort under "error"); set true (or pass --allow-first-bump) to apply the bump silently — legitimate when importing a package with prior external version history. |
| `strictReachable` | boolean | `false` | Only use reachable tags |
| `zeroMajor` | `"spec"` \| `"strict"` | `"spec"` | Pre-1.0 handling of commit-inferred breaking changes. 'spec' (default): bump the 0.x minor (0.24.0 → 0.25.0), per semver §4. 'strict': bump the next major (→ 1.0.0). Inferred path only — explicit overrides (--bump major, bump:major) always graduate to 1.0.0. |
| `npm` | object | — | npm/JavaScript version handling |
| `pub` | object | — | Dart/Flutter pub configuration |

Comment thread
greptile-apps[bot] marked this conversation as resolved.
### `version.zeroMajor`
Expand Down Expand Up @@ -127,13 +128,21 @@ ungrouped packages honor targets as-is.
**Workspace pins.** Intra-group `workspace:*` dependencies resolve to the group version within a
run, so a fixed group publishes internally consistent.

### `version.npm`

npm/JavaScript version handling.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `true` | Version package.json manifests. Detected npm packages are versioned by default; set false to opt out (e.g. npm versioning handled elsewhere). |

### `version.cargo`

Cargo/Rust configuration.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `true` | Enable Cargo.toml version handling |
| `enabled` | boolean | `true` | Version Cargo.toml manifests. Detected Rust packages are versioned by default; set false to opt out (e.g. a vendored crate, or Rust versioning handled elsewhere). |
| `paths` | `string[]` | — | Directories to search for Cargo.toml files |

---
Expand Down Expand Up @@ -162,7 +171,7 @@ NPM publishing configuration.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `true` | Enable NPM publishing |
| `enabled` | boolean | `true` | Publish to npm. Detected npm packages are published by default; set false to opt out (version and tag only). |
| `auth` | `"auto"` \| `"oidc"` \| `"token"` | `"auto"` | Authentication method |
| `provenance` | boolean | `true` | Enable npm provenance attestation |
| `access` | `"public"` \| `"restricted"` | `"public"` | Package access level |
Expand All @@ -177,7 +186,7 @@ Cargo publishing configuration.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `false` | Enable Cargo publishing |
| `enabled` | boolean | `true` | Publish to crates.io. Detected Rust packages are published by default; set false to opt out (version and tag only). |
| `noVerify` | boolean | `false` | Skip verification before publish |
| `publishOrder` | `string[]` | `[]` | Order in which to publish packages |
| `clean` | boolean | `false` | Clean before publishing |
Expand All @@ -188,7 +197,7 @@ Dart/Flutter publishing configuration via pub.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `enabled` | boolean | `false` | Enable pub.dev publishing |
| `enabled` | boolean | `true` | Publish to pub.dev. Detected Dart/Flutter packages are published by default; set false to opt out (version and tag only). |
| `publishOrder` | `string[]` | `[]` | Order in which to publish packages |

### `publish.githubRelease`
Expand Down
49 changes: 42 additions & 7 deletions packages/config/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,32 @@ export const MonorepoConfigSchema = z.object({
packagesPath: z.string().optional().describe('Path to packages directory'),
});

export const VersionNpmConfigSchema = z.object({
enabled: z
.boolean()
.default(true)
.describe(
'Version package.json manifests. Detected npm packages are versioned by default; set false to opt out (e.g. npm versioning handled elsewhere).',
),
});

export const VersionCargoConfigSchema = z.object({
enabled: z.boolean().default(true).describe('Enable Cargo.toml version handling'),
enabled: z
.boolean()
.default(true)
.describe(
'Version Cargo.toml manifests. Detected Rust packages are versioned by default; set false to opt out (e.g. a vendored crate, or Rust versioning handled elsewhere).',
),
paths: z.array(z.string()).optional().describe('Directories to search for Cargo.toml files'),
});

export const VersionPubConfigSchema = z.object({
enabled: z.boolean().default(true).describe('Enable pubspec.yaml version handling'),
enabled: z
.boolean()
.default(true)
.describe(
'Version pubspec.yaml manifests. Detected Dart/Flutter packages are versioned by default; set false to opt out.',
),
paths: z.array(z.string()).optional().describe('Directories to search for pubspec.yaml files'),
});

Expand Down Expand Up @@ -129,12 +148,18 @@ export const VersionConfigSchema = z.object({
.describe(
"Pre-1.0 handling of commit-inferred breaking changes. 'spec' (default): bump the 0.x minor (0.24.0 → 0.25.0), per semver §4. 'strict': bump the next major (→ 1.0.0). Inferred path only — explicit overrides (--bump major, bump:major) always graduate to 1.0.0.",
),
npm: VersionNpmConfigSchema.optional().describe('npm/JavaScript version handling'),
cargo: VersionCargoConfigSchema.optional().describe('Cargo/Rust configuration'),
pub: VersionPubConfigSchema.optional().describe('Dart/Flutter pub configuration'),
});

export const NpmConfigSchema = z.object({
enabled: z.boolean().default(true).describe('Enable NPM publishing'),
enabled: z
.boolean()
.default(true)
.describe(
'Publish to npm. Detected npm packages are published by default; set false to opt out (version and tag only).',
),
auth: z.enum(['auto', 'oidc', 'token']).default('auto').describe('Authentication method'),
provenance: z.boolean().default(true).describe('Enable npm provenance attestation'),
access: z.enum(['public', 'restricted']).default('public').describe('Package access level'),
Expand All @@ -148,14 +173,24 @@ export const NpmConfigSchema = z.object({
});

export const CargoPublishConfigSchema = z.object({
enabled: z.boolean().default(false).describe('Enable Cargo publishing'),
enabled: z
.boolean()
.default(true)
.describe(
'Publish to crates.io. Detected Rust packages are published by default; set false to opt out (version and tag only).',
),
noVerify: z.boolean().default(false).describe('Skip verification before publish'),
publishOrder: z.array(z.string()).default([]).describe('Order in which to publish packages'),
clean: z.boolean().default(false).describe('Clean before publishing'),
});

export const PubPublishConfigSchema = z.object({
enabled: z.boolean().default(false).describe('Enable pub.dev publishing'),
enabled: z
.boolean()
.default(true)
.describe(
'Publish to pub.dev. Detected Dart/Flutter packages are published by default; set false to opt out (version and tag only).',
),
publishOrder: z.array(z.string()).default([]).describe('Order in which to publish packages'),
});

Expand Down Expand Up @@ -248,13 +283,13 @@ export const PublishConfigSchema = z.object({
publishOrder: [],
}).describe('NPM publishing configuration'),
cargo: CargoPublishConfigSchema.default({
enabled: false,
enabled: true,
noVerify: false,
publishOrder: [],
clean: false,
}).describe('Cargo publishing configuration'),
pub: PubPublishConfigSchema.default({
enabled: false,
enabled: true,
publishOrder: [],
}).describe('Dart/Flutter publishing configuration via pub'),
githubRelease: GitHubReleaseConfigSchema.default({
Expand Down
15 changes: 11 additions & 4 deletions packages/config/test/unit/schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
TemplateConfigSchema,
VerifyConfigSchema,
VersionConfigSchema,
VersionNpmConfigSchema,
} from '../../src/schema.js';

describe('GitConfigSchema', () => {
Expand Down Expand Up @@ -64,7 +65,6 @@ describe('MonorepoConfigSchema', () => {
describe('VersionConfigSchema', () => {
it('should apply defaults', () => {
const result = VersionConfigSchema.parse({});
// biome-ignore lint/suspicious/noTemplateCurlyInString: checking the literal default string
expect(result.tagTemplate).toBe('${prefix}${version}');
expect(result.packageSpecificTags).toBe(false);
expect(result.preset).toBe('conventional');
Expand Down Expand Up @@ -104,6 +104,11 @@ describe('VersionConfigSchema', () => {
expect(result.cargo?.paths).toEqual(['crates/core', 'crates/cli']);
});

it('should default version.npm.enabled to true and accept an opt-out', () => {
expect(VersionNpmConfigSchema.parse({}).enabled).toBe(true);
expect(VersionConfigSchema.parse({ npm: { enabled: false } }).npm?.enabled).toBe(false);
});

it('should accept version groups with fixed and linked sync modes', () => {
const result = VersionConfigSchema.parse({
sync: false,
Expand Down Expand Up @@ -172,7 +177,8 @@ describe('NpmConfigSchema', () => {
describe('CargoPublishConfigSchema', () => {
it('should apply defaults', () => {
const result = CargoPublishConfigSchema.parse({});
expect(result.enabled).toBe(false);
// Detection enables, config opts out: crates.io publishing defaults on.
expect(result.enabled).toBe(true);
expect(result.noVerify).toBe(false);
expect(result.publishOrder).toEqual([]);
expect(result.clean).toBe(false);
Expand Down Expand Up @@ -221,10 +227,11 @@ describe('VerifyConfigSchema', () => {
});

describe('PublishConfigSchema', () => {
it('should apply defaults for npm and cargo', () => {
it('should default publishing on for npm, cargo, and pub (detection enables, config opts out)', () => {
const result = PublishConfigSchema.parse({});
expect(result.npm.enabled).toBe(true);
expect(result.cargo.enabled).toBe(false);
expect(result.cargo.enabled).toBe(true);
expect(result.pub.enabled).toBe(true);
expect(result.githubRelease.enabled).toBe(true);
});
});
Expand Down
8 changes: 6 additions & 2 deletions packages/version/src/core/groupStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,10 @@ async function releaseGroup(
}

const packageJsonPath = path.join(pkg.dir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
// npm version handling gates package.json manifests (default true).
if (config.npm?.enabled === false) {
log(`Skipping package.json update for ${name} - npm version handling disabled`, 'debug');
} else if (fs.existsSync(packageJsonPath)) {
updatePackageVersion(packageJsonPath, version, config.dryRun);
} else {
log(`Skipping package.json update for ${name} - no package.json found (Rust-only package)`, 'debug');
Expand Down Expand Up @@ -567,7 +570,8 @@ export function createGroupStrategy(config: Config): (packages: PackagesWithRoot

const name = pkg.packageJson.name;
const packageJsonPath = path.join(pkg.dir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
// npm version handling gates package.json manifests (default true).
if (config.npm?.enabled !== false && fs.existsSync(packageJsonPath)) {
updatePackageVersion(packageJsonPath, plan.ownNext, config.dryRun);
}
const cargoTomlPath = path.join(pkg.dir, 'Cargo.toml');
Expand Down
45 changes: 32 additions & 13 deletions packages/version/src/core/versionStrategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,29 @@ export function createSyncStrategy(config: Config): StrategyFunction {
// Check if packages.root is defined before joining paths
if (packages.root) {
const rootPkgPath = path.join(packages.root, 'package.json');
if (fs.existsSync(rootPkgPath)) {
// npm version handling gates package.json manifests (default true).
if (config.npm?.enabled !== false && fs.existsSync(rootPkgPath)) {
// Mark as root so consumers (preview, standing PR) can exclude this lockstep
// bump from package counts — it isn't a publishable package.
updatePackageVersion(rootPkgPath, nextVersion, dryRun, true);
files.push(rootPkgPath);
updatedPackages.push('root');
processedPaths.add(rootPkgPath);

// Handle Cargo.toml files in root
const rootCargoFiles = updateCargoFiles(packages.root, nextVersion, config.cargo, dryRun);
files.push(...rootCargoFiles);
}
// Record the root path unconditionally (even when npm is disabled) so the per-package loop
// skips it in a single-package repo where packages.root === packages[0].dir — otherwise its
// Cargo.toml would be versioned twice (once here, once in the loop).
processedPaths.add(rootPkgPath);

// Root Cargo.toml handling is independent of npm (updateCargoFiles honors cargo.enabled),
// so it runs whether or not npm versioning is disabled — matching the per-package loop below.
const rootCargoFiles = updateCargoFiles(packages.root, nextVersion, config.cargo, dryRun);
files.push(...rootCargoFiles);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
// Record 'root' when only the root Cargo.toml was versioned (npm disabled, or no root
// package.json). In a single-package repo where packages.root === packages[0].dir the loop
// skips this path via processedPaths, so without this nothing tracks the write and the empty
// updatedPackages triggers the early return below — abandoning the already-written file.
if (rootCargoFiles.length > 0 && !updatedPackages.includes('root')) {
updatedPackages.push('root');
}
} else {
log('Root package path is undefined, skipping root package.json update', 'warning');
Expand All @@ -258,8 +270,10 @@ export function createSyncStrategy(config: Config): StrategyFunction {
continue;
}

// Skip pure Rust packages that don't have a package.json
if (!fs.existsSync(packageJsonPath)) {
// Skip when npm version handling is opted out, or for pure Rust packages with no package.json.
if (config.npm?.enabled === false) {
log(`Skipping package.json update for ${pkg.packageJson.name} - npm version handling disabled`, 'debug');
} else if (!fs.existsSync(packageJsonPath)) {
log(
`Skipping package.json update for ${pkg.packageJson.name} - no package.json found (Rust-only package)`,
'debug',
Expand All @@ -275,8 +289,10 @@ export function createSyncStrategy(config: Config): StrategyFunction {
const pkgCargoFiles = updateCargoFiles(pkg.dir, nextVersion, config.cargo, dryRun);
files.push(...pkgCargoFiles);

// Track pure-Rust packages by name when their Cargo.toml was updated
if (!fs.existsSync(packageJsonPath) && pkgCargoFiles.length > 0) {
// Track by name when only the Cargo.toml was updated: a pure-Rust package (no package.json),
// or a hybrid package whose package.json write was skipped because npm handling is disabled.
// Without this the early return below abandons the already-written Cargo.toml changes.
if ((!fs.existsSync(packageJsonPath) || config.npm?.enabled === false) && pkgCargoFiles.length > 0) {
updatedPackages.push(pkg.packageJson.name);
}
}
Expand Down Expand Up @@ -641,12 +657,15 @@ export function createSingleStrategy(config: Config): StrategyFunction {
// Track all files that need to be committed
const filesToCommit: string[] = [];

// Only update package.json if it exists (skip for pure Rust packages)
if (fs.existsSync(packageJsonPath)) {
// Update package.json when npm version handling is enabled (default true) and it exists;
// otherwise skip (opted out, or a pure Rust package with no package.json).
const npmEnabled = config.npm?.enabled !== false;
if (npmEnabled && fs.existsSync(packageJsonPath)) {
updatePackageVersion(packageJsonPath, nextVersion, dryRun);
filesToCommit.push(packageJsonPath);
} else {
log(`Skipping package.json update for ${packageName} - no package.json found (Rust-only package)`, 'debug');
const reason = !npmEnabled ? 'npm version handling disabled' : 'no package.json found (Rust-only package)';
log(`Skipping package.json update for ${packageName} - ${reason}`, 'debug');
}

// Handle Cargo.toml files for this package
Expand Down
5 changes: 3 additions & 2 deletions packages/version/src/package/packageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,9 @@ export class PackageProcessor {
// This ensures consistent versioning across language ecosystems.
const packageJsonPath = path.join(pkgPath, 'package.json');

// Always update package.json if it exists
if (fs.existsSync(packageJsonPath)) {
// Update package.json when npm version handling is enabled (default true) and it exists.
const npmEnabled = this.fullConfig.npm?.enabled !== false;
if (npmEnabled && fs.existsSync(packageJsonPath)) {
updatePackageVersion(packageJsonPath, nextVersion, this.dryRun);
}

Expand Down
4 changes: 4 additions & 0 deletions packages/version/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ export interface Config extends VersionConfigBase {
strictReachable?: boolean;
/** Pre-1.0 inferred-breaking bump policy ('spec' | 'strict'). See docs/configuration.md#versionzeromajor. */
zeroMajor?: 'spec' | 'strict';
npm?: {
enabled?: boolean;
};
cargo?: {
enabled?: boolean;
paths?: string[];
Expand Down Expand Up @@ -240,6 +243,7 @@ export function toVersionConfig(config: VersionConfig | undefined): Config {
zeroMajor: config.zeroMajor,
versionPrefix: config.versionPrefix ?? '',
prereleaseIdentifier: config.prereleaseIdentifier,
npm: config.npm,
cargo: config.cargo,
pub: config.pub,
};
Expand Down
Loading
Loading