From f519c5b5af6e491abd13c6ca637940509d171f4c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Oct 2026 22:52:28 +0100 Subject: [PATCH 1/3] fix(publish): skip crates marked publish = false Publish's cargo discovery took every update directory with a Cargo.toml, so a crate with `publish = false` or `publish = []` (typically a napi-rs package's crate) was sent to `cargo publish`, failing the release. The version side already skipped these; both now share isCratePublishable. Closes #692 Co-Authored-By: Claude Opus 5.5 --- packages/config/src/cargo.ts | 6 +++++ packages/config/src/index.ts | 2 +- packages/publish/src/registry/cargo.ts | 5 +++++ .../test/unit/stages/cargo-publish.spec.ts | 22 +++++++++++++++++++ packages/version/src/core/versionEngine.ts | 6 ++--- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/config/src/cargo.ts b/packages/config/src/cargo.ts index d0b9a54a..a9f0ec26 100644 --- a/packages/config/src/cargo.ts +++ b/packages/config/src/cargo.ts @@ -14,6 +14,12 @@ export function parseCargoToml(cargoPath: string): CargoManifest { return TOML.parse(content) as CargoManifest; } +/** False for `publish = false` or `publish = []`, which keep a crate off every registry. */ +export function isCratePublishable(manifest: CargoManifest): boolean { + const publish = manifest.package?.publish; + return publish !== false && !(Array.isArray(publish) && publish.length === 0); +} + export function isCargoToml(filePath: string): boolean { return path.basename(filePath) === 'Cargo.toml'; } diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 2aa9f02b..beb395cb 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,4 +1,4 @@ -export { type CargoManifest, isCargoToml, parseCargoToml } from './cargo.js'; +export { type CargoManifest, isCargoToml, isCratePublishable, parseCargoToml } from './cargo.js'; export { ConfigError } from './errors.js'; export { type LoadOptions, diff --git a/packages/publish/src/registry/cargo.ts b/packages/publish/src/registry/cargo.ts index 6cefe8a2..7c0ddf43 100644 --- a/packages/publish/src/registry/cargo.ts +++ b/packages/publish/src/registry/cargo.ts @@ -1,5 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; +import { isCratePublishable } from '@releasekit/config'; import { buildDependencyGraph, debug, type GraphPackage } from '@releasekit/core'; import { createGitCli } from '@releasekit/git'; import { createPublishError, PublishErrorCode } from '../errors/index.js'; @@ -129,6 +130,10 @@ function findCrates(updates: Array<{ newVersion: string; filePath: string; dir: if (!cargo.package?.name) { continue; } + if (!isCratePublishable(cargo)) { + debug(`Skipping crate ${cargo.package.name}: publish = false`); + continue; + } crates.push({ packageName: cargo.package.name, diff --git a/packages/publish/test/unit/stages/cargo-publish.spec.ts b/packages/publish/test/unit/stages/cargo-publish.spec.ts index 6d7e6ab1..16166157 100644 --- a/packages/publish/test/unit/stages/cargo-publish.spec.ts +++ b/packages/publish/test/unit/stages/cargo-publish.spec.ts @@ -399,6 +399,28 @@ describe('cargo-publish stage', () => { expect((init?.headers as Record)?.['User-Agent']).toMatch(/releasekit/); }); + it.each(['publish = false', 'publish = []'])('should skip a crate with %s', async (publishLine) => { + const { execCommand } = await import('../../../src/utils/exec.js'); + const dir = createTmpDir(); + const pkgDir = path.join(dir, 'packages', 'napi'); + fs.mkdirSync(pkgDir, { recursive: true }); + fs.writeFileSync(path.join(pkgDir, 'package.json'), '{"name":"napi","version":"1.0.0"}'); + fs.writeFileSync(path.join(pkgDir, 'Cargo.toml'), `[package]\nname = "napi"\nversion = "1.0.0"\n${publishLine}\n`); + + const ctx = createContext(dir, { + input: { + dryRun: false, + updates: [{ packageName: 'napi', newVersion: '1.1.0', filePath: 'packages/napi/package.json' }], + changelogs: [], + tags: [], + }, + }); + await runCargoPublishStage(ctx); + + expect(vi.mocked(execCommand).mock.calls.filter((c) => c[0] === 'cargo')).toHaveLength(0); + expect(ctx.output.cargo).toHaveLength(0); + }); + it('should skip packages without Cargo.toml', async () => { const { execCommand } = await import('../../../src/utils/exec.js'); const dir = createTmpDir(); diff --git a/packages/version/src/core/versionEngine.ts b/packages/version/src/core/versionEngine.ts index ce9a4655..4bfe58d8 100644 --- a/packages/version/src/core/versionEngine.ts +++ b/packages/version/src/core/versionEngine.ts @@ -3,7 +3,7 @@ import * as path from 'node:path'; import { cwd } from 'node:process'; import { inspect } from 'node:util'; import { getPackagesSync, type Package, type Packages } from '@manypkg/get-packages'; -import { filterPackagesByConfig, parseCargoToml, parsePubspec } from '@releasekit/config'; +import { filterPackagesByConfig, isCratePublishable, parseCargoToml, parsePubspec } from '@releasekit/config'; import { isPrivatePackageJson, shouldMatchPackageTargets } from '@releasekit/core'; import { minimatch } from 'minimatch'; import * as yaml from 'yaml'; @@ -187,9 +187,7 @@ export class VersionEngine { // Parse Cargo.toml const cargoData = parseCargoToml(fullCargoPath); - // Skip crates that explicitly opt out of publishing (`publish = false` or `publish = []`). - const publishField = cargoData.package?.publish; - if (publishField === false || (Array.isArray(publishField) && publishField.length === 0)) { + if (!isCratePublishable(cargoData)) { log(`Skipping Rust package at ${packageDir}: publish = false`, 'debug'); continue; } From 888b5fe9fb6c43828ec6e65adeed0c2a89d2fd9d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Oct 2026 22:54:24 +0100 Subject: [PATCH 2/3] fix(publish): drop redundant isCratePublishable comment Co-Authored-By: Claude Opus 5.5 --- packages/config/src/cargo.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/config/src/cargo.ts b/packages/config/src/cargo.ts index a9f0ec26..5a34c865 100644 --- a/packages/config/src/cargo.ts +++ b/packages/config/src/cargo.ts @@ -14,7 +14,6 @@ export function parseCargoToml(cargoPath: string): CargoManifest { return TOML.parse(content) as CargoManifest; } -/** False for `publish = false` or `publish = []`, which keep a crate off every registry. */ export function isCratePublishable(manifest: CargoManifest): boolean { const publish = manifest.package?.publish; return publish !== false && !(Array.isArray(publish) && publish.length === 0); From 7e12f824e073118bb026f5d545719626c5c1d5a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Oct 2026 22:58:27 +0100 Subject: [PATCH 3/3] refactor(config): name the empty-registry-list case in isCratePublishable Co-Authored-By: Claude Opus 5.5 --- packages/config/src/cargo.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/config/src/cargo.ts b/packages/config/src/cargo.ts index 5a34c865..6527d0bd 100644 --- a/packages/config/src/cargo.ts +++ b/packages/config/src/cargo.ts @@ -16,7 +16,8 @@ export function parseCargoToml(cargoPath: string): CargoManifest { export function isCratePublishable(manifest: CargoManifest): boolean { const publish = manifest.package?.publish; - return publish !== false && !(Array.isArray(publish) && publish.length === 0); + const noRegistries = Array.isArray(publish) && publish.length === 0; + return publish !== false && !noRegistries; } export function isCargoToml(filePath: string): boolean {