From 40425a8845833927e5ae15a0cde92d4e0ff7cfb4 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 11:38:05 -0700 Subject: [PATCH 1/9] ci: allow tests to run on feature branch --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1e7fd5134..e01f20ccb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - feat/* pull_request: {} merge_group: From aafc7799e4c3c700abe5d8467e6283d5d5774d34 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 16:16:44 -0700 Subject: [PATCH 2/9] feat: monorepo dependency bumping Defaulting to "patch" changed behavior for existing configs: any package with {path, dependency} versioned_files entries would start receiving automatic releases on upgrade. Propagation is now opt-in. - internal_dependency_updates test now verifies the default does NOT propagate (dep strings still sync) - transitive test gets explicit config with "patch" policies, since auto-detected packages can't set one - with_changelog test sets "patch" explicitly, covering that value --- crates/knope-config/src/lib.rs | 2 +- crates/knope-config/src/package.rs | 30 +++ crates/knope-versioning/src/changes/mod.rs | 11 +- crates/knope-versioning/src/package.rs | 6 + .../knope-versioning/src/release_notes/mod.rs | 35 +++- .../src/versioned_file/mod.rs | 10 + crates/knope/src/config/package.rs | 12 +- .../knope/src/step/releases/internal_deps.rs | 187 ++++++++++++++++++ crates/knope/src/step/releases/mod.rs | 67 ++++++- crates/knope/src/step/releases/package.rs | 11 +- .../dryrun_stdout.log | 5 + .../internal_dependency_updates/in/Cargo.toml | 2 + .../in/first/Cargo.toml | 6 + .../internal_dependency_updates/in/knope.toml | 5 + .../in/second/Cargo.toml | 3 + .../internal_dependency_updates/mod.rs | 18 ++ .../out/first/Cargo.toml | 6 + .../out/second/Cargo.toml | 3 + .../dryrun_stdout.log | 7 + .../in/Cargo.toml | 5 + .../in/first/Cargo.toml | 3 + .../in/knope.toml | 18 ++ .../in/second/Cargo.toml | 3 + .../mod.rs | 19 ++ .../out/Cargo.toml | 5 + .../out/first/Cargo.toml | 3 + .../out/second/Cargo.toml | 3 + .../in/Cargo.toml | 2 + .../in/first/Cargo.toml | 6 + .../in/knope.toml | 17 ++ .../in/second/Cargo.toml | 3 + .../internal_dependency_updates_minor/mod.rs | 16 ++ .../out/first/Cargo.toml | 6 + .../out/second/Cargo.toml | 3 + .../dryrun_stdout.log | 5 + .../in/Cargo.toml | 2 + .../in/first/Cargo.toml | 6 + .../in/knope.toml | 17 ++ .../in/second/Cargo.toml | 3 + .../mod.rs | 20 ++ .../out/first/Cargo.toml | 6 + .../out/second/Cargo.toml | 3 + .../in/Cargo.toml | 2 + .../in/first/Cargo.toml | 6 + .../in/knope.toml | 17 ++ .../in/second/Cargo.toml | 3 + .../internal_dependency_updates_none/mod.rs | 16 ++ .../out/first/Cargo.toml | 6 + .../out/second/Cargo.toml | 3 + .../dryrun_stdout.log | 5 + .../in/Cargo.lock | 11 ++ .../in/Cargo.toml | 2 + .../in/first/Cargo.toml | 3 + .../in/knope.toml | 17 ++ .../in/second/Cargo.toml | 3 + .../mod.rs | 19 ++ .../out/Cargo.lock | 11 ++ .../out/first/Cargo.toml | 3 + .../out/second/Cargo.toml | 3 + .../in/Cargo.toml | 2 + .../in/a/Cargo.toml | 6 + .../in/b/Cargo.toml | 6 + .../in/c/Cargo.toml | 3 + .../in/knope.toml | 19 ++ .../mod.rs | 19 ++ .../out/a/Cargo.toml | 6 + .../out/b/Cargo.toml | 6 + .../out/c/Cargo.toml | 3 + .../dryrun_stdout.log | 22 +++ .../in/Cargo.toml | 2 + .../in/first/CHANGELOG.md | 1 + .../in/first/Cargo.toml | 6 + .../in/knope.toml | 19 ++ .../in/second/CHANGELOG.md | 1 + .../in/second/Cargo.toml | 3 + .../mod.rs | 16 ++ .../out/first/CHANGELOG.md | 7 + .../out/first/Cargo.toml | 6 + .../out/second/CHANGELOG.md | 6 + .../out/second/Cargo.toml | 3 + .../prepare_release/cargo/workspace/mod.rs | 8 + .../dryrun_stdout.log | 8 +- .../major_version_directories/stdout.log | 8 +- .../prepare_release/verbose/dryrun_stdout.log | 6 +- .../tests/prepare_release/verbose/stdout.log | 6 +- .../docs/recipes/updating-dependencies.md | 29 +++ .../docs/reference/Config File/packages.mdx | 72 +++++++ 87 files changed, 1002 insertions(+), 27 deletions(-) create mode 100644 crates/knope/src/step/releases/internal_deps.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.lock create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/Cargo.lock create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/a/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/b/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/c/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/a/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/b/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/c/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/CHANGELOG.md create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/CHANGELOG.md create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/CHANGELOG.md create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/CHANGELOG.md create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/Cargo.toml diff --git a/crates/knope-config/src/lib.rs b/crates/knope-config/src/lib.rs index 913e66bbf..189020251 100644 --- a/crates/knope-config/src/lib.rs +++ b/crates/knope-config/src/lib.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; pub use self::{ changelog_section::ChangelogSection, - package::{Asset, AssetNameError, Assets, Package, VersionedFile}, + package::{Asset, AssetNameError, Assets, InternalDependencyUpdate, Package, VersionedFile}, template::{Template, Variable}, }; pub use crate::release_notes::ReleaseNotes; diff --git a/crates/knope-config/src/package.rs b/crates/knope-config/src/package.rs index 1f88c705c..0a91dba04 100644 --- a/crates/knope-config/src/package.rs +++ b/crates/knope-config/src/package.rs @@ -24,6 +24,36 @@ pub struct Package { pub assets: Option, #[serde(default, skip_serializing_if = "<&bool>::not")] pub ignore_go_major_versioning: bool, + /// What kind of bump this package gets when one of its internal monorepo dependencies bumps. + #[serde(default, skip_serializing_if = "InternalDependencyUpdate::is_default")] + pub update_internal_dependencies: InternalDependencyUpdate, + /// Names of other packages in this config that this package depends on. Usually Knope + /// derives these relationships from `versioned_files` `dependency` entries; use this to + /// declare ones it can't see (for example, versions tracked only in a shared workspace + /// manifest). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub internal_dependencies: Vec, +} + +/// Policy for how this package is versioned when one of its internal monorepo dependencies +/// receives a release. +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum InternalDependencyUpdate { + /// Don't bump this package when an internal dependency bumps (the default). + #[default] + None, + /// Bump this package as a patch release when an internal dependency bumps. + Patch, + /// Bump this package as a minor release when an internal dependency bumps. + Minor, +} + +impl InternalDependencyUpdate { + #[must_use] + pub fn is_default(&self) -> bool { + matches!(self, Self::None) + } } /// A type that deserializes from either a single regex string or an array of regex strings. diff --git a/crates/knope-versioning/src/changes/mod.rs b/crates/knope-versioning/src/changes/mod.rs index 0a9f175fc..5ed303c15 100644 --- a/crates/knope-versioning/src/changes/mod.rs +++ b/crates/knope-versioning/src/changes/mod.rs @@ -172,8 +172,14 @@ impl From> for ChangeType { #[derive(Clone, Debug, Eq, PartialEq)] pub enum ChangeSource { - ConventionalCommit { description: String }, - ChangeFile { id: Arc }, + ConventionalCommit { + description: String, + }, + ChangeFile { + id: Arc, + }, + /// Synthesized by Knope when propagating a release through internal monorepo dependencies. + DependencyUpdate, } impl Display for ChangeSource { @@ -184,6 +190,7 @@ impl Display for ChangeSource { .. } => write!(f, "commit {message}"), Self::ChangeFile { id, .. } => write!(f, "changeset {}", id.to_file_name()), + Self::DependencyUpdate => write!(f, "dependency update"), } } } diff --git a/crates/knope-versioning/src/package.rs b/crates/knope-versioning/src/package.rs index 863ea303e..02c76b27e 100644 --- a/crates/knope-versioning/src/package.rs +++ b/crates/knope-versioning/src/package.rs @@ -193,6 +193,12 @@ impl Package { self.versions.latest() } + /// The versioned-file configs associated with this package. + #[must_use] + pub fn versioned_files(&self) -> &[Config] { + &self.versioned_files + } + /// Apply a Rule to a [`PackageVersion`], incrementing & resetting the correct components. /// /// # Versions 0.x diff --git a/crates/knope-versioning/src/release_notes/mod.rs b/crates/knope-versioning/src/release_notes/mod.rs index 8d58ed436..2829498e3 100644 --- a/crates/knope-versioning/src/release_notes/mod.rs +++ b/crates/knope-versioning/src/release_notes/mod.rs @@ -7,7 +7,12 @@ pub use release::Release; use serde::Deserialize; use time::{OffsetDateTime, macros::format_description}; -use crate::{Action, changes::Change, package, semver::Version}; +use crate::{ + Action, + changes::{Change, ChangeSource}, + package, + semver::Version, +}; mod changelog; mod config; @@ -46,7 +51,10 @@ impl ReleaseNotes { for (section_name, sources) in self.sections.iter() { let mut changes = changes .iter() - .filter(|change| sources.contains(&change.change_type)) + .filter(|change| { + sources.contains(&change.change_type) + && change.original_source != ChangeSource::DependencyUpdate + }) .sorted() .peekable(); if changes.peek().is_some() { @@ -59,6 +67,7 @@ impl ReleaseNotes { write_body(&mut notes, changes, &self.change_templates); } } + write_dependency_updates(&mut notes, changes); let release = Release { title: release_title(&version)?, @@ -95,6 +104,28 @@ impl ReleaseNotes { )] pub struct TimeError(#[from] time::error::Format); +/// Append a "Dependencies" section listing internal monorepo dependency bumps in changesets +/// style: a single bullet "Updated dependencies" with each `name@version` nested under it. +fn write_dependency_updates(out: &mut String, changes: &[Change]) { + let dep_changes: Vec<&Change> = changes + .iter() + .filter(|c| c.original_source == ChangeSource::DependencyUpdate) + .collect(); + if dep_changes.is_empty() { + return; + } + if !out.is_empty() { + out.push_str("\n\n"); + } + out.push_str("## Dependencies\n\n- Updated dependencies"); + for change in dep_changes { + if let Some(details) = change.details.as_ref() { + out.push('\n'); + out.push_str(details); + } + } +} + fn write_body<'change>( out: &mut String, changes: Peekable>, diff --git a/crates/knope-versioning/src/versioned_file/mod.rs b/crates/knope-versioning/src/versioned_file/mod.rs index 4646fb18c..b94c90b5b 100644 --- a/crates/knope-versioning/src/versioned_file/mod.rs +++ b/crates/knope-versioning/src/versioned_file/mod.rs @@ -495,6 +495,16 @@ impl Config { self.path.clone() } + /// Whether this file is a lock file (recording resolved versions) rather than a manifest + /// (declaring a package or its dependencies). + #[must_use] + pub fn is_lock_file(&self) -> bool { + matches!( + self.format, + Format::CargoLock | Format::PackageLockJson | Format::DenoLock + ) + } + #[must_use] pub fn to_pathbuf(&self) -> PathBuf { self.as_path().to_path("") diff --git a/crates/knope/src/config/package.rs b/crates/knope/src/config/package.rs index 55209362c..a7ef247d7 100644 --- a/crates/knope/src/config/package.rs +++ b/crates/knope/src/config/package.rs @@ -17,7 +17,7 @@ use deno_path_util::url_to_file_path; use deno_semver::package::PackageKind; use glob::glob; use itertools::Itertools; -use knope_config::{Assets, ChangelogSection}; +use knope_config::{Assets, ChangelogSection, InternalDependencyUpdate}; use knope_versioning::{ ConfigError, UnknownFile, VersionedFileConfig, package, versioned_file::cargo, }; @@ -59,6 +59,8 @@ pub struct Package { pub(crate) extra_changelog_sections: Vec, pub(crate) assets: Option, pub(crate) ignore_go_major_versioning: bool, + pub(crate) update_internal_dependencies: InternalDependencyUpdate, + pub(crate) internal_dependencies: Vec, } impl Package { @@ -181,6 +183,8 @@ impl Package { extra_changelog_sections: vec![], assets: None, ignore_go_major_versioning: false, + update_internal_dependencies: InternalDependencyUpdate::default(), + internal_dependencies: Vec::new(), } }) .collect()) @@ -485,6 +489,8 @@ impl Package { extra_changelog_sections, assets, ignore_go_major_versioning, + update_internal_dependencies, + internal_dependencies, } = package; let versioned_files = versioned_files .into_iter() @@ -518,6 +524,8 @@ impl Package { extra_changelog_sections, assets, ignore_go_major_versioning, + update_internal_dependencies, + internal_dependencies, }) } } @@ -612,6 +620,8 @@ impl From for knope_config::Package { extra_changelog_sections: package.extra_changelog_sections, assets: package.assets, ignore_go_major_versioning: package.ignore_go_major_versioning, + update_internal_dependencies: package.update_internal_dependencies, + internal_dependencies: package.internal_dependencies, } } } diff --git a/crates/knope/src/step/releases/internal_deps.rs b/crates/knope/src/step/releases/internal_deps.rs new file mode 100644 index 000000000..67932c001 --- /dev/null +++ b/crates/knope/src/step/releases/internal_deps.rs @@ -0,0 +1,187 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +use knope_config::InternalDependencyUpdate; +use knope_versioning::{ + changes::{Change, ChangeSource, ChangeType}, + package::Name, + semver::Version, +}; +use relative_path::RelativePathBuf; +use tracing::debug; + +use super::Package; + +/// A bumped dependency that should be reflected in a dependent's release notes. +#[derive(Clone, Debug)] +pub(super) struct BumpedDependency { + pub name: Name, + pub new_version: Version, +} + +/// Build dependents-of edges: `dependents[D]` = indices of packages that depend on D. +/// +/// A package P "owns" a path if it has a `versioned_files` entry for that path with no +/// `dependency` set (i.e., the file defines P's own version). A `{ path, dependency }` entry +/// in package D's `versioned_files` means D's version gets written into that file, so when +/// the file is owned by some other package O, that implies the edge "O depends on D" +/// (regardless of the name D goes by inside the file). Lock files record versions rather +/// than declare dependencies, so they neither own nor imply edges. Packages can also declare +/// dependencies explicitly via `internal_dependencies` for relationships that aren't visible +/// in `versioned_files` (e.g., versions tracked only in a shared workspace manifest). +/// +/// Edges to dependents that opted out (`update_internal_dependencies = "none"`) are skipped +/// entirely: they can't receive propagation, and including them would only change the order +/// in which packages are processed. +pub(super) fn build_dependents(packages: &[Package]) -> HashMap> { + let mut owners: HashMap = HashMap::new(); + for (idx, pkg) in packages.iter().enumerate() { + for vf in pkg.versioning.versioned_files() { + if vf.dependency.is_none() && !vf.is_lock_file() { + owners.insert(vf.as_path(), idx); + } + } + } + let name_to_idx: HashMap<&str, usize> = packages + .iter() + .enumerate() + .map(|(idx, pkg)| (pkg.name().as_ref(), idx)) + .collect(); + + let mut edges: HashMap> = HashMap::new(); + // (dependent, dependency) pairs already recorded + let mut seen: HashSet<(usize, usize)> = HashSet::new(); + let mut add_edge = |dependency_idx: usize, dependent_idx: usize, seen: &mut HashSet<_>| { + let Some(dependency) = packages.get(dependency_idx) else { + return; + }; + let Some(dependent) = packages.get(dependent_idx) else { + return; + }; + if dependent_idx == dependency_idx + || dependent.update_internal_dependencies == InternalDependencyUpdate::None + || !seen.insert((dependent_idx, dependency_idx)) + { + return; + } + edges + .entry(dependency.name().clone()) + .or_default() + .push(dependent_idx); + }; + + for (dependency_idx, pkg) in packages.iter().enumerate() { + for vf in pkg.versioning.versioned_files() { + if vf.dependency.is_none() || vf.is_lock_file() { + continue; + } + let Some(&dependent_idx) = owners.get(&vf.as_path()) else { + continue; + }; + add_edge(dependency_idx, dependent_idx, &mut seen); + } + } + + for (dependent_idx, pkg) in packages.iter().enumerate() { + for dep_name in &pkg.internal_dependencies { + let Some(&dependency_idx) = name_to_idx.get(dep_name.as_str()) else { + debug!( + "internal_dependencies of {pkg} names unknown package {dep_name}; ignoring", + pkg = pkg.name() + ); + continue; + }; + add_edge(dependency_idx, dependent_idx, &mut seen); + } + } + edges +} + +/// Topologically sort package indices so that a package appears after all of its internal +/// dependencies. Falls back to original order on cycles (we still return every index). +#[expect( + clippy::indexing_slicing, + reason = "all indices come from 0..packages.len() or from `dependents` values that were built from those same indices" +)] +pub(super) fn topological_order( + packages: &[Package], + dependents: &HashMap>, +) -> Vec { + let n = packages.len(); + let mut in_degree = vec![0usize; n]; + for targets in dependents.values() { + for &t in targets { + if t < n { + in_degree[t] += 1; + } + } + } + // FIFO so that packages with no dependency relationship keep their config order. + let mut queue: VecDeque = (0..n).filter(|&i| in_degree[i] == 0).collect(); + let mut visited = vec![false; n]; + let mut order = Vec::with_capacity(n); + while let Some(idx) = queue.pop_front() { + if visited[idx] { + continue; + } + visited[idx] = true; + order.push(idx); + if let Some(targets) = dependents.get(packages[idx].name()) { + for &t in targets { + if t >= n || visited[t] { + continue; + } + in_degree[t] = in_degree[t].saturating_sub(1); + if in_degree[t] == 0 { + queue.push_back(t); + } + } + } + } + // Append any remaining (would happen if there's a cycle) — better to release them than panic. + for idx in 0..n { + if !visited[idx] { + debug!( + "Internal dependency cycle detected; falling back to source order for {pkg}", + pkg = packages[idx].name() + ); + order.push(idx); + } + } + order +} + +/// Build a synthetic [`Change`] representing "this package was bumped because its internal +/// dependencies updated." The change is grouped with any others on the same package, and +/// rendered as an "Updated dependencies" section in the release notes. +pub(super) fn synthetic_change( + policy: InternalDependencyUpdate, + bumps: &[BumpedDependency], +) -> Option { + match policy { + InternalDependencyUpdate::None => None, + InternalDependencyUpdate::Patch | InternalDependencyUpdate::Minor => { + let change_type = match policy { + InternalDependencyUpdate::Minor => ChangeType::Feature, + _ => ChangeType::Fix, + }; + let details = bumps + .iter() + .map(|bump| { + format!( + " - {name}@{version}", + name = bump.name, + version = bump.new_version + ) + }) + .collect::>() + .join("\n"); + Some(Change { + change_type, + summary: "Updated dependencies".to_string(), + details: Some(details), + original_source: ChangeSource::DependencyUpdate, + git: None, + }) + } + } +} diff --git a/crates/knope/src/step/releases/mod.rs b/crates/knope/src/step/releases/mod.rs index a3c8c7c12..016043ff2 100644 --- a/crates/knope/src/step/releases/mod.rs +++ b/crates/knope/src/step/releases/mod.rs @@ -23,6 +23,7 @@ pub(crate) mod changelog; pub(crate) mod conventional_commits; pub(crate) mod gitea; pub(crate) mod github; +mod internal_deps; pub(crate) mod package; pub(crate) mod semver; @@ -48,7 +49,9 @@ pub(crate) async fn prepare_release( Vec::new() }; - for package in &mut state.packages { + let mut per_package_changes: Vec> = + Vec::with_capacity(state.packages.len()); + for package in &state.packages { let mut changes = package.gather_changes( prepare_release, &state.all_git_tags, @@ -81,13 +84,11 @@ pub(crate) async fn prepare_release( )); } } - - let (all_versioned_files, actions) = - package.apply_release(&changes, prepare_release, state.all_versioned_files)?; - state.all_versioned_files = all_versioned_files; - state.pending_actions.extend(actions); + per_package_changes.push(changes); } + apply_releases_with_propagation(&mut state, per_package_changes, prepare_release)?; + let actions = state .all_versioned_files .drain(..) @@ -114,6 +115,60 @@ pub(crate) fn bump_version(state: RunType, rule: &Rule) -> Result>, + prepare_release: &PrepareRelease, +) -> Result<(), Error> { + let dependents = internal_deps::build_dependents(&state.packages); + let order = internal_deps::topological_order(&state.packages, &dependents); + + // Accumulates dependency bumps to be folded into a dependent's synthetic change before it + // is applied. Keyed by the dependent's index in `state.packages`. + let mut pending_dep_bumps: Vec> = + vec![Vec::new(); state.packages.len()]; + + for idx in order { + let mut changes = std::mem::take(&mut per_package_changes[idx]); + + let bumps = std::mem::take(&mut pending_dep_bumps[idx]); + if !bumps.is_empty() { + let policy = state.packages[idx].update_internal_dependencies; + if let Some(synthetic) = internal_deps::synthetic_change(policy, &bumps) { + changes.push(synthetic); + } + } + + let package = &mut state.packages[idx]; + let bumped = !changes.is_empty() || package.override_version.is_some(); + let versioned_files = std::mem::take(&mut state.all_versioned_files); + let (all_versioned_files, actions) = + package.apply_release(&changes, prepare_release, versioned_files)?; + state.all_versioned_files = all_versioned_files; + state.pending_actions.extend(actions); + + if bumped + && let Some(new_version) = package.versioning.latest_version() + && let Some(targets) = dependents.get(package.name()) + { + for &target in targets { + pending_dep_bumps[target].push(internal_deps::BumpedDependency { + name: package.name().clone(), + new_version: new_version.clone(), + }); + } + } + } + Ok(()) +} + #[derive(Debug, Diagnostic, thiserror::Error)] pub(crate) enum Error { #[error("No packages are ready to release")] diff --git a/crates/knope/src/step/releases/package.rs b/crates/knope/src/step/releases/package.rs index 08134da40..5aeb89941 100644 --- a/crates/knope/src/step/releases/package.rs +++ b/crates/knope/src/step/releases/package.rs @@ -3,7 +3,7 @@ use std::{fmt, fmt::Display, path::PathBuf}; use changesets::PackageChange; use indexmap::IndexMap; use itertools::Itertools; -use knope_config::{Assets, changelog_section::convert_to_versioning}; +use knope_config::{Assets, InternalDependencyUpdate, changelog_section::convert_to_versioning}; use knope_versioning::{ Action, GoVersioning, PackageNewError, VersionedFile, VersionedFileError, changes::{CHANGESET_DIR, Change}, @@ -31,6 +31,11 @@ pub(crate) struct Package { pub(crate) override_version: Option, pub(crate) assets: Option, pub(crate) go_versioning: GoVersioning, + /// Bump policy when one of this package's internal monorepo dependencies releases. + pub(crate) update_internal_dependencies: InternalDependencyUpdate, + /// Explicitly-declared names of packages this package depends on, supplementing the + /// relationships derived from `versioned_files`. + pub(crate) internal_dependencies: Vec, } impl Package { @@ -92,6 +97,8 @@ impl Package { GoVersioning::default() }, override_version: None, + update_internal_dependencies: package.update_internal_dependencies, + internal_dependencies: package.internal_dependencies, }) } @@ -273,6 +280,8 @@ impl Package { override_version: None, assets: None, go_versioning: GoVersioning::default(), + update_internal_dependencies: InternalDependencyUpdate::default(), + internal_dependencies: Vec::new(), } } } diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/dryrun_stdout.log b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/dryrun_stdout.log new file mode 100644 index 000000000..951dc4d68 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/dryrun_stdout.log @@ -0,0 +1,5 @@ +Would add the following to first/Cargo.toml: second-package.version = 0.1.1 +Would add the following to second/Cargo.toml: version = 0.1.1 +Would add files to git: + first/Cargo.toml + second/Cargo.toml diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/first/Cargo.toml new file mode 100644 index 000000000..5f8c39eba --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-package = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/knope.toml new file mode 100644 index 000000000..247c178c4 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/knope.toml @@ -0,0 +1,5 @@ +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/mod.rs new file mode 100644 index 000000000..bd8b8965c --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/mod.rs @@ -0,0 +1,18 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// By default, a dependent gets its dependency strings updated but is _not_ released itself — +/// propagation is opt-in via `update_internal_dependencies`. +#[test] +fn dependents_are_not_bumped_by_default() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("feat(second-package): A feature in the dep"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/first/Cargo.toml new file mode 100644 index 000000000..65ca6dbc7 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-package = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/dryrun_stdout.log b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/dryrun_stdout.log new file mode 100644 index 000000000..5d066cfd4 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/dryrun_stdout.log @@ -0,0 +1,7 @@ +Would add the following to first/Cargo.toml: version = 1.0.1 +Would add the following to second/Cargo.toml: version = 0.1.1 +Would add the following to Cargo.toml: second-package.version = 0.1.1 +Would add files to git: + first/Cargo.toml + second/Cargo.toml + Cargo.toml diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/Cargo.toml new file mode 100644 index 000000000..ebeebe004 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = ["first", "second"] + +[workspace.dependencies] +second-package = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/first/Cargo.toml new file mode 100644 index 000000000..6efed9837 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/knope.toml new file mode 100644 index 000000000..e844b2c01 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/knope.toml @@ -0,0 +1,18 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +scopes = ["first-package"] +update_internal_dependencies = "patch" +internal_dependencies = ["second-package"] + +[packages.second-package] +versioned_files = [ + "second/Cargo.toml", + { path = "Cargo.toml", dependency = "second-package" }, +] +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/mod.rs new file mode 100644 index 000000000..18ab465db --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/mod.rs @@ -0,0 +1,19 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// When a dependency's version is only tracked in a shared workspace manifest (owned by no +/// package), Knope can't derive the relationship — `internal_dependencies` declares it +/// explicitly so the dependent still gets released. +#[test] +fn explicit_internal_dependencies_propagate() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("fix(second-package): A fix in second"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/Cargo.toml new file mode 100644 index 000000000..801e3d27a --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = ["first", "second"] + +[workspace.dependencies] +second-package = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/first/Cargo.toml new file mode 100644 index 000000000..ba564fc15 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_explicit/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/first/Cargo.toml new file mode 100644 index 000000000..5f8c39eba --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-package = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/knope.toml new file mode 100644 index 000000000..97a88da47 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/knope.toml @@ -0,0 +1,17 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +scopes = ["first-package"] +update_internal_dependencies = "minor" + +[packages.second-package] +versioned_files = [ + "second/Cargo.toml", + { path = "first/Cargo.toml", dependency = "second-package" }, +] +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/mod.rs new file mode 100644 index 000000000..4f4f7b529 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/mod.rs @@ -0,0 +1,16 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +#[test] +fn dependent_with_minor_policy_gets_minor_bump() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("fix(second-package): A fix in the dep"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/first/Cargo.toml new file mode 100644 index 000000000..0dfa47de4 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.1.0" + +[dependencies] +second-package = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_minor/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/dryrun_stdout.log b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/dryrun_stdout.log new file mode 100644 index 000000000..71762d196 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/dryrun_stdout.log @@ -0,0 +1,5 @@ +Would add the following to first/Cargo.toml: second-lib.version = 0.1.1, version = 1.0.1 +Would add the following to second/Cargo.toml: version = 0.1.1 +Would add files to git: + first/Cargo.toml + second/Cargo.toml diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/first/Cargo.toml new file mode 100644 index 000000000..a37b9e42e --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-lib = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/knope.toml new file mode 100644 index 000000000..b932cfd73 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/knope.toml @@ -0,0 +1,17 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +scopes = ["first-package"] +update_internal_dependencies = "patch" + +[packages.second-package] +versioned_files = [ + "second/Cargo.toml", + { path = "first/Cargo.toml", dependency = "second-lib" }, +] +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/mod.rs new file mode 100644 index 000000000..06e28e960 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/mod.rs @@ -0,0 +1,20 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// Propagation works even when the name a dependency goes by inside a file (here: the +/// published crate name `second-lib`) differs from its package key (`second-package`) — +/// the relationship comes from which package's `versioned_files` contains the entry, not +/// from matching the name. +#[test] +fn propagates_when_crate_name_differs_from_package_key() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("fix(second-package): A fix in second"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/first/Cargo.toml new file mode 100644 index 000000000..8344b3b44 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.1" + +[dependencies] +second-lib = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_name_mismatch/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/first/Cargo.toml new file mode 100644 index 000000000..5f8c39eba --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-package = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/knope.toml new file mode 100644 index 000000000..a60cb1e28 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/knope.toml @@ -0,0 +1,17 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +scopes = ["first-package"] +update_internal_dependencies = "none" + +[packages.second-package] +versioned_files = [ + "second/Cargo.toml", + { path = "first/Cargo.toml", dependency = "second-package" }, +] +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/mod.rs new file mode 100644 index 000000000..96e010f58 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/mod.rs @@ -0,0 +1,16 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +#[test] +fn dependent_with_none_policy_is_not_bumped() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("fix(second-package): A fix in the dep"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/first/Cargo.toml new file mode 100644 index 000000000..65ca6dbc7 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-package = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_none/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/dryrun_stdout.log b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/dryrun_stdout.log new file mode 100644 index 000000000..023aeb2aa --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/dryrun_stdout.log @@ -0,0 +1,5 @@ +Would add the following to Cargo.lock: second-renamed = 0.1.1 +Would add the following to second/Cargo.toml: version = 0.1.1 +Would add files to git: + Cargo.lock + second/Cargo.toml diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.lock b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.lock new file mode 100644 index 000000000..59df21f6c --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.lock @@ -0,0 +1,11 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "first-package" +version = "1.0.0" + +[[package]] +name = "second-renamed" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/first/Cargo.toml new file mode 100644 index 000000000..6efed9837 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/knope.toml new file mode 100644 index 000000000..af5e85c30 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/knope.toml @@ -0,0 +1,17 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml", "Cargo.lock"] +scopes = ["first-package"] +update_internal_dependencies = "patch" + +[packages.second-package] +versioned_files = [ + "second/Cargo.toml", + { path = "Cargo.lock", dependency = "second-renamed" }, +] +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/mod.rs new file mode 100644 index 000000000..cb9a3db03 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/mod.rs @@ -0,0 +1,19 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// Sharing a lock file is not a dependency relationship: `first-package` owns no manifest +/// that `second-package` writes into, so `second-package`'s release must not propagate to +/// it even though `first-package` opted into patch bumps. +#[test] +fn shared_lock_file_does_not_propagate() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("fix(second-package): A fix in second"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/Cargo.lock b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/Cargo.lock new file mode 100644 index 000000000..659ad4f94 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/Cargo.lock @@ -0,0 +1,11 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "first-package" +version = "1.0.0" + +[[package]] +name = "second-renamed" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/first/Cargo.toml new file mode 100644 index 000000000..6efed9837 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_shared_lock/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/Cargo.toml new file mode 100644 index 000000000..e7d118cea --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["a", "b", "c"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/a/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/a/Cargo.toml new file mode 100644 index 000000000..26ceeeacf --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/a/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "a" +version = "1.0.0" + +[dependencies] +b = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/b/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/b/Cargo.toml new file mode 100644 index 000000000..6e6b507d7 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "b" +version = "1.0.0" + +[dependencies] +c = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/c/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/c/Cargo.toml new file mode 100644 index 000000000..34b515883 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/c/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "c" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/knope.toml new file mode 100644 index 000000000..1910ec6ff --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/in/knope.toml @@ -0,0 +1,19 @@ +[packages.a] +versioned_files = ["a/Cargo.toml"] +scopes = ["a"] +update_internal_dependencies = "patch" + +[packages.b] +versioned_files = ["b/Cargo.toml", { path = "a/Cargo.toml", dependency = "b" }] +scopes = ["b"] +update_internal_dependencies = "patch" + +[packages.c] +versioned_files = ["c/Cargo.toml", { path = "b/Cargo.toml", dependency = "c" }] +scopes = ["c"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/mod.rs new file mode 100644 index 000000000..c5b3c76d1 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/mod.rs @@ -0,0 +1,19 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// Verify that bumps propagate transitively: `c` bumps, `b` (depends on c) gets a patch bump, +/// then `a` (depends on b) also gets a patch bump. +#[test] +fn propagates_transitively() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("a/v1.0.0"), + Tag("b/v1.0.0"), + Tag("c/v1.0.0"), + Commit("feat(c)!: A breaking change in c"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/a/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/a/Cargo.toml new file mode 100644 index 000000000..bfdd0d115 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/a/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "a" +version = "1.0.1" + +[dependencies] +b = "1.0.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/b/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/b/Cargo.toml new file mode 100644 index 000000000..3204104be --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "b" +version = "1.0.1" + +[dependencies] +c = "2.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/c/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/c/Cargo.toml new file mode 100644 index 000000000..525903611 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_transitive/out/c/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "c" +version = "2.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/dryrun_stdout.log b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/dryrun_stdout.log new file mode 100644 index 000000000..b7c588dbe --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/dryrun_stdout.log @@ -0,0 +1,22 @@ +Would add the following to first/Cargo.toml: second-package.version = 0.1.1, version = 1.0.1 +Would add the following to second/Cargo.toml: version = 0.1.1 +Would add the following to second/CHANGELOG.md: +## 0.1.1 ([DATE]) + +### Features + +- A feature in the dep + +Would add the following to first/CHANGELOG.md: +## 1.0.1 ([DATE]) + +### Dependencies + +- Updated dependencies + - second-package@0.1.1 + +Would add files to git: + first/Cargo.toml + second/Cargo.toml + second/CHANGELOG.md + first/CHANGELOG.md diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/CHANGELOG.md b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/Cargo.toml new file mode 100644 index 000000000..5f8c39eba --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-package = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/knope.toml new file mode 100644 index 000000000..a677466d0 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/knope.toml @@ -0,0 +1,19 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +changelog = "first/CHANGELOG.md" +scopes = ["first-package"] +update_internal_dependencies = "patch" + +[packages.second-package] +versioned_files = [ + "second/Cargo.toml", + { path = "first/Cargo.toml", dependency = "second-package" }, +] +changelog = "second/CHANGELOG.md" +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/CHANGELOG.md b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/CHANGELOG.md new file mode 100644 index 000000000..825c32f0d --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/mod.rs new file mode 100644 index 000000000..570ef1d17 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/mod.rs @@ -0,0 +1,16 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +#[test] +fn dependent_changelog_gets_updated_dependencies_section() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("feat(second-package): A feature in the dep"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/CHANGELOG.md b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/CHANGELOG.md new file mode 100644 index 000000000..46df97215 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog +## 1.0.1 ([DATE]) + +### Dependencies + +- Updated dependencies + - second-package@0.1.1 diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/Cargo.toml new file mode 100644 index 000000000..8849f19e5 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.1" + +[dependencies] +second-package = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/CHANGELOG.md b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/CHANGELOG.md new file mode 100644 index 000000000..fe258d4cb --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +## 0.1.1 ([DATE]) + +### Features + +- A feature in the dep diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_with_changelog/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/mod.rs index d0fd21359..d791d0498 100644 --- a/crates/knope/tests/prepare_release/cargo/workspace/mod.rs +++ b/crates/knope/tests/prepare_release/cargo/workspace/mod.rs @@ -1,3 +1,11 @@ mod full; +mod internal_dependency_updates; +mod internal_dependency_updates_explicit; +mod internal_dependency_updates_minor; +mod internal_dependency_updates_name_mismatch; +mod internal_dependency_updates_none; +mod internal_dependency_updates_shared_lock; +mod internal_dependency_updates_transitive; +mod internal_dependency_updates_with_changelog; mod no_lock; mod shared_version; diff --git a/crates/knope/tests/prepare_release/go_modules/major_version_directories/dryrun_stdout.log b/crates/knope/tests/prepare_release/go_modules/major_version_directories/dryrun_stdout.log index e34dee2ed..4938782a3 100644 --- a/crates/knope/tests/prepare_release/go_modules/major_version_directories/dryrun_stdout.log +++ b/crates/knope/tests/prepare_release/go_modules/major_version_directories/dryrun_stdout.log @@ -13,14 +13,14 @@ Getting conventional commits since last release of package v1 No tags found starting with v1/v Using ALL commits Only checking commits with scopes: ["v1"] -Determining new version for v1 -commit fix(v1): A fix - implies rule PATCH -Using PATCH rule to bump from 1.0.0 to 1.0.1 Getting conventional commits since last release of package v2 No tags found starting with v2/v Using ALL commits Only checking commits with scopes: ["v2"] +Determining new version for v1 +commit fix(v1): A fix + implies rule PATCH +Using PATCH rule to bump from 1.0.0 to 1.0.1 Determining new version for v2 commit feat(v2): New feature implies rule MINOR diff --git a/crates/knope/tests/prepare_release/go_modules/major_version_directories/stdout.log b/crates/knope/tests/prepare_release/go_modules/major_version_directories/stdout.log index 36a98f417..cf5ba2524 100644 --- a/crates/knope/tests/prepare_release/go_modules/major_version_directories/stdout.log +++ b/crates/knope/tests/prepare_release/go_modules/major_version_directories/stdout.log @@ -13,14 +13,14 @@ Getting conventional commits since last release of package v1 No tags found starting with v1/v Using ALL commits Only checking commits with scopes: ["v1"] -Determining new version for v1 -commit fix(v1): A fix - implies rule PATCH -Using PATCH rule to bump from 1.0.0 to 1.0.1 Getting conventional commits since last release of package v2 No tags found starting with v2/v Using ALL commits Only checking commits with scopes: ["v2"] +Determining new version for v1 +commit fix(v1): A fix + implies rule PATCH +Using PATCH rule to bump from 1.0.0 to 1.0.1 Determining new version for v2 commit feat(v2): New feature implies rule MINOR diff --git a/crates/knope/tests/prepare_release/verbose/dryrun_stdout.log b/crates/knope/tests/prepare_release/verbose/dryrun_stdout.log index 449c28c22..ae770243d 100644 --- a/crates/knope/tests/prepare_release/verbose/dryrun_stdout.log +++ b/crates/knope/tests/prepare_release/verbose/dryrun_stdout.log @@ -9,6 +9,9 @@ Running step PrepareRelease(PrepareRelease { prerelease_label: None, allow_empty Getting conventional commits since last release of package first Using commits since tag first/v1.2.3 Only checking commits with scopes: ["first"] +Getting conventional commits since last release of package second +Using commits since tag second/v0.4.6 +Only checking commits with scopes: ["second"] Determining new version for first commit feat: A feature implies rule MINOR @@ -33,9 +36,6 @@ changeset breaking_change.md changeset feature.md implies rule MINOR Using MAJOR rule to bump from 1.2.3 to 2.0.0 -Getting conventional commits since last release of package second -Using commits since tag second/v0.4.6 -Only checking commits with scopes: ["second"] Determining new version for second commit feat: A feature implies rule MINOR diff --git a/crates/knope/tests/prepare_release/verbose/stdout.log b/crates/knope/tests/prepare_release/verbose/stdout.log index 611b19462..cfd689e06 100644 --- a/crates/knope/tests/prepare_release/verbose/stdout.log +++ b/crates/knope/tests/prepare_release/verbose/stdout.log @@ -9,6 +9,9 @@ Running step PrepareRelease(PrepareRelease { prerelease_label: None, allow_empty Getting conventional commits since last release of package first Using commits since tag first/v1.2.3 Only checking commits with scopes: ["first"] +Getting conventional commits since last release of package second +Using commits since tag second/v0.4.6 +Only checking commits with scopes: ["second"] Determining new version for first commit feat: A feature implies rule MINOR @@ -33,9 +36,6 @@ changeset breaking_change.md changeset feature.md implies rule MINOR Using MAJOR rule to bump from 1.2.3 to 2.0.0 -Getting conventional commits since last release of package second -Using commits since tag second/v0.4.6 -Only checking commits with scopes: ["second"] Determining new version for second commit feat: A feature implies rule MINOR diff --git a/docs/src/content/docs/recipes/updating-dependencies.md b/docs/src/content/docs/recipes/updating-dependencies.md index 64d762bcc..7590acf7a 100644 --- a/docs/src/content/docs/recipes/updating-dependencies.md +++ b/docs/src/content/docs/recipes/updating-dependencies.md @@ -65,3 +65,32 @@ versioned_files = [ You can use this same `dependency` field to override the name of a package updated in a `Cargo.lock`. ::: + +## Releasing dependents automatically + +Wiring up `versioned_files` keeps the dependency _string_ inside dependent packages in sync, +but by default each dependent must still have its own [changeset](/reference/concepts/change) +to actually trigger a release of that dependent. + +To release dependents automatically whenever one of their internal dependencies releases, set +`update_internal_dependencies` on the dependent's package config to `"patch"` or `"minor"` — +see the +[`update_internal_dependencies` reference](/reference/config-file/packages#update_internal_dependencies) +for details. The default is `"none"`: dependency strings are kept in sync, but no release +is created for the dependent. + +For example, given the setup above where `knope` depends on `knope-versioning`, setting +`update_internal_dependencies = "patch"` on the `knope` package means releasing +`knope-versioning` will also release `knope` as a patch, and the new release notes for +`knope` will include a `Dependencies` section listing the bumped dependencies. + +:::caution + +Knope derives the dependency relationships from `versioned_files` entries that point at +another package's files. If a dependency's version is only tracked in a shared file that +belongs to no package — for example, in the workspace-root `Cargo.toml` via +`[workspace.dependencies]` — declare the relationship explicitly with +[`internal_dependencies`](/reference/config-file/packages#internal_dependencies) on the +dependent's package config. + +::: diff --git a/docs/src/content/docs/reference/Config File/packages.mdx b/docs/src/content/docs/reference/Config File/packages.mdx index 78debbe78..a7113ab59 100644 --- a/docs/src/content/docs/reference/Config File/packages.mdx +++ b/docs/src/content/docs/reference/Config File/packages.mdx @@ -486,3 +486,75 @@ ignore_go_major_versioning = true To maintain multiple major versions of a Go module, check out [this recipe](/recipes/multiple-major-go-versions) ::: + +## `update_internal_dependencies` + +When a package is part of a monorepo and depends on another package in the same repo, +Knope can automatically release the dependent whenever the dependency is released. + +Set `update_internal_dependencies` on the dependent's package config to control the bump: + +- `"none"` (default): do not release the dependent when its dependency changes. + The dependency's new version is still written into the dependent's files (so the build + stays consistent), but no version bump or release notes are produced for the dependent. +- `"patch"`: release the dependent as a patch bump. +- `"minor"`: release the dependent as a minor bump. + +```toml title="knope.toml" +[packages.consumer] +versioned_files = ["consumer/Cargo.toml"] +update_internal_dependencies = "patch" # or "minor"; defaults to "none" +``` + +When this propagation triggers a release, Knope adds a "Dependencies" section to the +dependent's release notes, in the style of the +[changesets](https://github.com/changesets/changesets) tool: + +```markdown +## 1.0.1 (2026-06-04) + +### Dependencies + +- Updated dependencies + - my-dependency@1.2.3 +``` + +Propagation is transitive: if `A` depends on `B`, and `B` depends on `C`, then releasing `C` +will bump `B`, which will in turn bump `A` (subject to each package's own +`update_internal_dependencies` setting). + +### How Knope finds dependents + +A `versioned_files` entry like `{ path = "consumer/Cargo.toml", dependency = "my-lib" }` in +the `my-lib` package's config means "when `my-lib` releases, write its new version into +`consumer/Cargo.toml`". When that file is also a versioned file of another package +(`consumer` here), Knope records that `consumer` depends on `my-lib` — the name the +dependency goes by _inside_ the file doesn't need to match its package name in `knope.toml`. + +Lock files (like `Cargo.lock` or `package-lock.json`) record versions for the whole +workspace, so sharing one does **not** create a dependency relationship. + +If a dependency's version is only tracked somewhere Knope can't attribute to the dependent — +for example, a `[workspace.dependencies]` entry in a shared workspace manifest — declare the +relationship explicitly with [`internal_dependencies`](#internal_dependencies). + +## `internal_dependencies` + +Explicitly declares which other packages (by their name in `knope.toml`) this package +depends on, for [`update_internal_dependencies`](#update_internal_dependencies) propagation. +This supplements the relationships Knope derives from `versioned_files`, covering cases +where the dependency's version isn't tracked in one of this package's own files: + +```toml title="knope.toml" +[packages.my-lib] +versioned_files = [ + "crates/my-lib/Cargo.toml", + # my-lib's version is tracked in the shared workspace manifest, which no package owns + { path = "Cargo.toml", dependency = "my-lib" }, +] + +[packages.consumer] +versioned_files = ["crates/consumer/Cargo.toml"] +update_internal_dependencies = "patch" +internal_dependencies = ["my-lib"] +``` From 57e973e9d555266511fc59247bf31bf553e85d01 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 16:38:13 -0700 Subject: [PATCH 3/9] feat: derive internal dependencies from manifest contents Knope already parses every versioned file at release time, so the dependency graph for propagation can be read straight from manifests: if an opted-in package's Cargo.toml or package.json declares a dependency matching another package's manifest name, the relationship is detected with no configuration beyond update_internal_dependencies. This is the same information the workspace auto-detection uses at config-load time, applied to explicit [packages] configs. Dependency strings inside manifests are still only updated through explicit {path, dependency} versioned_files entries; internal_dependencies remains the override for relationships neither mechanism can see. Co-Authored-By: Claude Fable 5 --- .../src/versioned_file/mod.rs | 24 +++++++++ .../src/versioned_file/package_json.rs | 16 ++++++ .../knope/src/step/releases/internal_deps.rs | 49 ++++++++++++++++++- crates/knope/src/step/releases/mod.rs | 2 +- .../dryrun_stdout.log | 5 ++ .../in/Cargo.toml | 2 + .../in/first/Cargo.toml | 6 +++ .../in/knope.toml | 14 ++++++ .../in/second/Cargo.toml | 3 ++ .../mod.rs | 20 ++++++++ .../out/first/Cargo.toml | 6 +++ .../out/second/Cargo.toml | 3 ++ .../prepare_release/cargo/workspace/mod.rs | 1 + .../dryrun_stdout.log | 5 ++ .../in/first/package.json | 7 +++ .../in/knope.toml | 14 ++++++ .../in/second/package.json | 4 ++ .../internal_dependency_from_manifest/mod.rs | 18 +++++++ .../out/first/package.json | 7 +++ .../out/second/package.json | 4 ++ crates/knope/tests/prepare_release/npm/mod.rs | 1 + .../docs/recipes/updating-dependencies.md | 9 ++-- .../docs/reference/Config File/packages.mdx | 25 +++++++--- 23 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/first/package.json create mode 100644 crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/second/package.json create mode 100644 crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/mod.rs create mode 100644 crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/first/package.json create mode 100644 crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/second/package.json diff --git a/crates/knope-versioning/src/versioned_file/mod.rs b/crates/knope-versioning/src/versioned_file/mod.rs index b94c90b5b..3a2f0072f 100644 --- a/crates/knope-versioning/src/versioned_file/mod.rs +++ b/crates/knope-versioning/src/versioned_file/mod.rs @@ -117,6 +117,30 @@ impl VersionedFile { } } + /// The package name this file declares, if it's a manifest that declares one. + /// Currently supported for `Cargo.toml` and `package.json`. + #[must_use] + pub fn package_name(&self) -> Option { + match self { + VersionedFile::Cargo(cargo) => { + cargo::name_from_document(&cargo.document).map(String::from) + } + VersionedFile::PackageJson(package_json) => package_json.package_name(), + _ => None, + } + } + + /// Whether this file is a manifest declaring a dependency on `name`. + /// Currently supported for `Cargo.toml` and `package.json`. + #[must_use] + pub fn declares_dependency(&self, name: &str) -> bool { + match self { + VersionedFile::Cargo(cargo) => cargo::contains_dependency(&cargo.document, name), + VersionedFile::PackageJson(package_json) => package_json.declares_dependency(name), + _ => false, + } + } + #[must_use] pub fn path(&self) -> &RelativePathBuf { match self { diff --git a/crates/knope-versioning/src/versioned_file/package_json.rs b/crates/knope-versioning/src/versioned_file/package_json.rs index da39df9f0..97638b2fe 100644 --- a/crates/knope-versioning/src/versioned_file/package_json.rs +++ b/crates/knope-versioning/src/versioned_file/package_json.rs @@ -38,6 +38,22 @@ impl PackageJson { &self.path } + /// The `name` field of the manifest, if present. + pub(crate) fn package_name(&self) -> Option { + let json = serde_json::from_str::>(&self.raw).ok()?; + json.get("name")?.as_str().map(String::from) + } + + /// Whether the manifest declares a dependency (or dev dependency) on `name`. + pub(crate) fn declares_dependency(&self, name: &str) -> bool { + let Ok(json) = serde_json::from_str::>(&self.raw) else { + return false; + }; + ["dependencies", "devDependencies"] + .iter() + .any(|key| json.get(*key).and_then(|deps| deps.get(name)).is_some()) + } + pub(crate) fn set_version( mut self, new_version: &Version, diff --git a/crates/knope/src/step/releases/internal_deps.rs b/crates/knope/src/step/releases/internal_deps.rs index 67932c001..ef3f8ebc7 100644 --- a/crates/knope/src/step/releases/internal_deps.rs +++ b/crates/knope/src/step/releases/internal_deps.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use knope_config::InternalDependencyUpdate; use knope_versioning::{ + VersionedFile, changes::{Change, ChangeSource, ChangeType}, package::Name, semver::Version, @@ -29,10 +30,17 @@ pub(super) struct BumpedDependency { /// dependencies explicitly via `internal_dependencies` for relationships that aren't visible /// in `versioned_files` (e.g., versions tracked only in a shared workspace manifest). /// +/// For packages that opted in, Knope also reads the manifests themselves: if a package's +/// manifest (`Cargo.toml`, `package.json`) declares a dependency whose name matches another +/// package's manifest name, that relationship is detected with no extra configuration. +/// /// Edges to dependents that opted out (`update_internal_dependencies = "none"`) are skipped /// entirely: they can't receive propagation, and including them would only change the order /// in which packages are processed. -pub(super) fn build_dependents(packages: &[Package]) -> HashMap> { +pub(super) fn build_dependents( + packages: &[Package], + all_versioned_files: &[VersionedFile], +) -> HashMap> { let mut owners: HashMap = HashMap::new(); for (idx, pkg) in packages.iter().enumerate() { for vf in pkg.versioning.versioned_files() { @@ -93,6 +101,45 @@ pub(super) fn build_dependents(packages: &[Package]) -> HashMap add_edge(dependency_idx, dependent_idx, &mut seen); } } + + // Manifest-derived edges: each package's manifests (the parsed contents of its + // non-dependency, non-lock versioned files) declare dependencies by name; match those + // names against the other packages' manifest names. + let manifests: Vec> = packages + .iter() + .map(|pkg| { + pkg.versioning + .versioned_files() + .iter() + .filter(|config| config.dependency.is_none() && !config.is_lock_file()) + .filter_map(|config| { + all_versioned_files + .iter() + .find(|vf| *vf.path() == config.as_path()) + }) + .collect() + }) + .collect(); + let manifest_names: Vec> = manifests + .iter() + .map(|files| files.iter().filter_map(|vf| vf.package_name()).collect()) + .collect(); + for (dependent_idx, (pkg, files)) in packages.iter().zip(&manifests).enumerate() { + if pkg.update_internal_dependencies == InternalDependencyUpdate::None { + continue; + } + for (dependency_idx, names) in manifest_names.iter().enumerate() { + if dependency_idx == dependent_idx { + continue; + } + if names + .iter() + .any(|name| files.iter().any(|vf| vf.declares_dependency(name))) + { + add_edge(dependency_idx, dependent_idx, &mut seen); + } + } + } edges } diff --git a/crates/knope/src/step/releases/mod.rs b/crates/knope/src/step/releases/mod.rs index 016043ff2..f3c19651a 100644 --- a/crates/knope/src/step/releases/mod.rs +++ b/crates/knope/src/step/releases/mod.rs @@ -127,7 +127,7 @@ fn apply_releases_with_propagation( mut per_package_changes: Vec>, prepare_release: &PrepareRelease, ) -> Result<(), Error> { - let dependents = internal_deps::build_dependents(&state.packages); + let dependents = internal_deps::build_dependents(&state.packages, &state.all_versioned_files); let order = internal_deps::topological_order(&state.packages, &dependents); // Accumulates dependency bumps to be folded into a dependent's synthetic change before it diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/dryrun_stdout.log b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/dryrun_stdout.log new file mode 100644 index 000000000..aa0b1bc5a --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/dryrun_stdout.log @@ -0,0 +1,5 @@ +Would add the following to first/Cargo.toml: version = 1.0.1 +Would add the following to second/Cargo.toml: version = 0.1.1 +Would add files to git: + first/Cargo.toml + second/Cargo.toml diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/first/Cargo.toml new file mode 100644 index 000000000..5f8c39eba --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.0" + +[dependencies] +second-package = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/knope.toml new file mode 100644 index 000000000..ec7f9f48a --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/knope.toml @@ -0,0 +1,14 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +scopes = ["first-package"] +update_internal_dependencies = "patch" + +[packages.second-package] +versioned_files = ["second/Cargo.toml"] +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/mod.rs new file mode 100644 index 000000000..b5c5e878f --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/mod.rs @@ -0,0 +1,20 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// With no `{ path, dependency }` entries and no `internal_dependencies`, the relationship +/// is read from the manifests themselves: `first-package`'s `Cargo.toml` declares a +/// dependency on `second-package`, so opting in to `update_internal_dependencies` is all +/// the configuration needed. +#[test] +fn relationships_are_read_from_manifests() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("fix(second-package): A fix in second"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/first/Cargo.toml new file mode 100644 index 000000000..ab302e281 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/first/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "first-package" +version = "1.0.1" + +[dependencies] +second-package = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/internal_dependency_updates_from_manifest/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/mod.rs index d791d0498..927992afc 100644 --- a/crates/knope/tests/prepare_release/cargo/workspace/mod.rs +++ b/crates/knope/tests/prepare_release/cargo/workspace/mod.rs @@ -1,6 +1,7 @@ mod full; mod internal_dependency_updates; mod internal_dependency_updates_explicit; +mod internal_dependency_updates_from_manifest; mod internal_dependency_updates_minor; mod internal_dependency_updates_name_mismatch; mod internal_dependency_updates_none; diff --git a/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/dryrun_stdout.log b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/dryrun_stdout.log new file mode 100644 index 000000000..d3acd0d26 --- /dev/null +++ b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/dryrun_stdout.log @@ -0,0 +1,5 @@ +Would add the following to first/package.json: version = 1.0.1 +Would add the following to second/package.json: version = 0.1.1 +Would add files to git: + first/package.json + second/package.json diff --git a/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/first/package.json b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/first/package.json new file mode 100644 index 000000000..cbef58687 --- /dev/null +++ b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/first/package.json @@ -0,0 +1,7 @@ +{ + "name": "first-package", + "version": "1.0.0", + "dependencies": { + "second-package": "^0.1.0" + } +} diff --git a/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/knope.toml b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/knope.toml new file mode 100644 index 000000000..24377b0a9 --- /dev/null +++ b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/knope.toml @@ -0,0 +1,14 @@ +[packages.first-package] +versioned_files = ["first/package.json"] +scopes = ["first-package"] +update_internal_dependencies = "patch" + +[packages.second-package] +versioned_files = ["second/package.json"] +scopes = ["second-package"] + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/second/package.json b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/second/package.json new file mode 100644 index 000000000..83f53a9aa --- /dev/null +++ b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/in/second/package.json @@ -0,0 +1,4 @@ +{ + "name": "second-package", + "version": "0.1.0" +} diff --git a/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/mod.rs b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/mod.rs new file mode 100644 index 000000000..360996c49 --- /dev/null +++ b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/mod.rs @@ -0,0 +1,18 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// Same as the Cargo workspace test, but for `package.json` manifests: the dependency on +/// `second-package` is read from `first/package.json` with no extra configuration. +#[test] +fn relationships_are_read_from_package_json() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Commit("fix(second-package): A fix in second"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/first/package.json b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/first/package.json new file mode 100644 index 000000000..501f947ea --- /dev/null +++ b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/first/package.json @@ -0,0 +1,7 @@ +{ + "name": "first-package", + "version": "1.0.1", + "dependencies": { + "second-package": "^0.1.0" + } +} \ No newline at end of file diff --git a/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/second/package.json b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/second/package.json new file mode 100644 index 000000000..744353728 --- /dev/null +++ b/crates/knope/tests/prepare_release/npm/internal_dependency_from_manifest/out/second/package.json @@ -0,0 +1,4 @@ +{ + "name": "second-package", + "version": "0.1.1" +} \ No newline at end of file diff --git a/crates/knope/tests/prepare_release/npm/mod.rs b/crates/knope/tests/prepare_release/npm/mod.rs index 3e417d914..15f4e8e25 100644 --- a/crates/knope/tests/prepare_release/npm/mod.rs +++ b/crates/knope/tests/prepare_release/npm/mod.rs @@ -1,3 +1,4 @@ mod dependencies; +mod internal_dependency_from_manifest; mod package; mod workspaces; diff --git a/docs/src/content/docs/recipes/updating-dependencies.md b/docs/src/content/docs/recipes/updating-dependencies.md index 7590acf7a..ad9d0f045 100644 --- a/docs/src/content/docs/recipes/updating-dependencies.md +++ b/docs/src/content/docs/recipes/updating-dependencies.md @@ -86,10 +86,11 @@ For example, given the setup above where `knope` depends on `knope-versioning`, :::caution -Knope derives the dependency relationships from `versioned_files` entries that point at -another package's files. If a dependency's version is only tracked in a shared file that -belongs to no package — for example, in the workspace-root `Cargo.toml` via -`[workspace.dependencies]` — declare the relationship explicitly with +Knope finds the dependency relationships by reading each opted-in package's manifests +(`Cargo.toml`, `package.json`) and from `versioned_files` entries that point at another +package's files. If a relationship isn't visible to either — for example, the version is +only tracked in the workspace-root `Cargo.toml` via `[workspace.dependencies]` — declare it +explicitly with [`internal_dependencies`](/reference/config-file/packages#internal_dependencies) on the dependent's package config. diff --git a/docs/src/content/docs/reference/Config File/packages.mdx b/docs/src/content/docs/reference/Config File/packages.mdx index a7113ab59..d2c9f8258 100644 --- a/docs/src/content/docs/reference/Config File/packages.mdx +++ b/docs/src/content/docs/reference/Config File/packages.mdx @@ -525,18 +525,27 @@ will bump `B`, which will in turn bump `A` (subject to each package's own ### How Knope finds dependents -A `versioned_files` entry like `{ path = "consumer/Cargo.toml", dependency = "my-lib" }` in -the `my-lib` package's config means "when `my-lib` releases, write its new version into -`consumer/Cargo.toml`". When that file is also a versioned file of another package -(`consumer` here), Knope records that `consumer` depends on `my-lib` — the name the -dependency goes by _inside_ the file doesn't need to match its package name in `knope.toml`. +For any package that opted in, Knope reads its manifests (currently `Cargo.toml` and +`package.json`): if a manifest declares a dependency whose name matches another package's +manifest name, the relationship is detected automatically — no extra configuration needed. +Note that this alone doesn't keep the dependency's version string inside the manifest up to +date; add a `{ path, dependency }` entry to the dependency's `versioned_files` if you want +that written on every release. + +Those `{ path, dependency }` entries also imply relationships: an entry like +`{ path = "consumer/Cargo.toml", dependency = "my-lib" }` in the `my-lib` package's config +means "when `my-lib` releases, write its new version into `consumer/Cargo.toml`". When that +file is also a versioned file of another package (`consumer` here), Knope records that +`consumer` depends on `my-lib` — the name the dependency goes by _inside_ the file doesn't +need to match its package name in `knope.toml`. Lock files (like `Cargo.lock` or `package-lock.json`) record versions for the whole workspace, so sharing one does **not** create a dependency relationship. -If a dependency's version is only tracked somewhere Knope can't attribute to the dependent — -for example, a `[workspace.dependencies]` entry in a shared workspace manifest — declare the -relationship explicitly with [`internal_dependencies`](#internal_dependencies). +If a relationship isn't visible to either mechanism — for example, the manifest format isn't +supported yet, or the version is only tracked in a shared workspace manifest via +`[workspace.dependencies]` — declare it explicitly with +[`internal_dependencies`](#internal_dependencies). ## `internal_dependencies` From 5622b75da51c89dd4e4f7cf72dcfd7183ec701f3 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 22:45:39 -0700 Subject: [PATCH 4/9] chore: add changesets Co-Authored-By: Claude Fable 5 --- ...atically_release_dependents_in_monorepos.md | 18 ++++++++++++++++++ .changeset/internal_dependency_api_changes.md | 11 +++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .changeset/automatically_release_dependents_in_monorepos.md create mode 100644 .changeset/internal_dependency_api_changes.md diff --git a/.changeset/automatically_release_dependents_in_monorepos.md b/.changeset/automatically_release_dependents_in_monorepos.md new file mode 100644 index 000000000..f475aa295 --- /dev/null +++ b/.changeset/automatically_release_dependents_in_monorepos.md @@ -0,0 +1,18 @@ +--- +knope: minor +--- + +# Automatically release dependents when internal dependencies change + +In a monorepo, setting `update_internal_dependencies = "patch"` (or `"minor"`) on a package now releases that package whenever one of its in-repo dependencies releases. The new release gets a changesets-style `Dependencies` section listing what triggered it, and propagation is transitive—releases are applied in dependency order. + +Knope finds the relationships automatically: from each opted-in package's manifest (`Cargo.toml` or `package.json` dependencies), and from `versioned_files` entries that point at another package's files. For anything neither can see (for example, versions tracked only in a workspace-root manifest), declare the relationship explicitly with `internal_dependencies`: + +```toml +[packages.consumer] +versioned_files = ["consumer/Cargo.toml"] +update_internal_dependencies = "patch" +internal_dependencies = ["my-lib"] # usually unnecessary +``` + +The default is `"none"`—nothing changes unless a package opts in. See the [`update_internal_dependencies` documentation](https://knope.tech/reference/config-file/packages#update_internal_dependencies) for details. diff --git a/.changeset/internal_dependency_api_changes.md b/.changeset/internal_dependency_api_changes.md new file mode 100644 index 000000000..de777d0f6 --- /dev/null +++ b/.changeset/internal_dependency_api_changes.md @@ -0,0 +1,11 @@ +--- +versioning: major +config: major +--- + +# New APIs for internal dependency propagation + +To support automatic monorepo dependency updates: + +- `knope-versioning`: `ChangeSource` has a new `DependencyUpdate` variant (breaking for exhaustive matches). `Package::versioned_files()`, `Config::is_lock_file()`, `VersionedFile::package_name()`, and `VersionedFile::declares_dependency()` were added. +- `knope-config`: `Package` has new `update_internal_dependencies` and `internal_dependencies` fields (breaking for struct-literal construction), and the `InternalDependencyUpdate` enum was added. From 179813973b5e288e023e547668a47aa4d5c454f1 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 16:16:44 -0700 Subject: [PATCH 5/9] feat: tracking packages by paths touched, not just scopes I like having computers do work I will inevitably forget to do. Tests added: - track_paths_basic: commits route to packages via versioned-file dirs - track_paths_explicit: explicit paths override the fallback - track_paths_multi: one commit can apply to multiple packages - track_paths_orphan: commits outside every territory are dropped --- crates/knope-config/src/package.rs | 9 +++ .../src/changes/conventional_commit.rs | 4 + crates/knope/src/config/package.rs | 10 +++ crates/knope/src/integrations/git.rs | 36 +++++++++ crates/knope/src/step/releases/package.rs | 80 ++++++++++++++++++- .../prepare_release/cargo/workspace/mod.rs | 4 + .../workspace/track_paths_basic/in/Cargo.toml | 2 + .../track_paths_basic/in/first/Cargo.toml | 3 + .../track_paths_basic/in/first/lib.rs | 1 + .../workspace/track_paths_basic/in/knope.toml | 13 +++ .../track_paths_basic/in/second/Cargo.toml | 3 + .../track_paths_basic/in/second/lib.rs | 1 + .../cargo/workspace/track_paths_basic/mod.rs | 21 +++++ .../track_paths_basic/out/first/Cargo.toml | 3 + .../track_paths_basic/out/second/Cargo.toml | 3 + .../track_paths_explicit/in/Cargo.toml | 2 + .../track_paths_explicit/in/docs/guide.md | 3 + .../track_paths_explicit/in/first/Cargo.toml | 3 + .../track_paths_explicit/in/knope.toml | 14 ++++ .../track_paths_explicit/in/second/Cargo.toml | 3 + .../track_paths_explicit/in/second/lib.rs | 1 + .../workspace/track_paths_explicit/mod.rs | 21 +++++ .../track_paths_explicit/out/first/Cargo.toml | 3 + .../out/second/Cargo.toml | 3 + .../workspace/track_paths_multi/in/Cargo.toml | 2 + .../track_paths_multi/in/first/Cargo.toml | 3 + .../track_paths_multi/in/first/lib.rs | 1 + .../workspace/track_paths_multi/in/knope.toml | 13 +++ .../track_paths_multi/in/second/Cargo.toml | 3 + .../track_paths_multi/in/second/lib.rs | 1 + .../cargo/workspace/track_paths_multi/mod.rs | 19 +++++ .../track_paths_multi/out/first/Cargo.toml | 3 + .../track_paths_multi/out/second/Cargo.toml | 3 + .../track_paths_orphan/in/Cargo.toml | 2 + .../workspace/track_paths_orphan/in/README.md | 3 + .../track_paths_orphan/in/first/Cargo.toml | 3 + .../track_paths_orphan/in/first/lib.rs | 1 + .../track_paths_orphan/in/knope.toml | 13 +++ .../track_paths_orphan/in/second/Cargo.toml | 3 + .../cargo/workspace/track_paths_orphan/mod.rs | 22 +++++ .../track_paths_orphan/out/first/Cargo.toml | 3 + .../track_paths_orphan/out/second/Cargo.toml | 3 + .../docs/reference/Config File/packages.mdx | 47 +++++++++++ 43 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/lib.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/lib.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/docs/guide.md create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/lib.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/lib.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/lib.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/README.md create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/lib.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/second/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/mod.rs create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/first/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/second/Cargo.toml diff --git a/crates/knope-config/src/package.rs b/crates/knope-config/src/package.rs index 0a91dba04..8b8ae1cb4 100644 --- a/crates/knope-config/src/package.rs +++ b/crates/knope-config/src/package.rs @@ -33,6 +33,15 @@ pub struct Package { /// manifest). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub internal_dependencies: Vec, + /// If `true`, conventional commits are routed to this package by the files they changed + /// rather than (or in addition to) the commit's scope. + #[serde(default, skip_serializing_if = "<&bool>::not")] + pub track_paths: bool, + /// Explicit list of directories/files that belong to this package (used when + /// `track_paths` is `true`). If empty, falls back to the parent directories of the + /// package's own (non-dependency) `versioned_files`. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub paths: Vec, } /// Policy for how this package is versioned when one of its internal monorepo dependencies diff --git a/crates/knope-versioning/src/changes/conventional_commit.rs b/crates/knope-versioning/src/changes/conventional_commit.rs index f4e3a1575..1d63b1964 100644 --- a/crates/knope-versioning/src/changes/conventional_commit.rs +++ b/crates/knope-versioning/src/changes/conventional_commit.rs @@ -1,4 +1,5 @@ use git_conventional::{Commit as ConventionalCommit, Footer, Type}; +use relative_path::RelativePathBuf; use tracing::debug; use super::{Change, ChangeSource, ChangeType, GitInfo}; @@ -8,6 +9,9 @@ use crate::release_notes::Sections; pub struct Commit { pub message: String, pub info: Option, + /// Files changed by this commit relative to the repo root. Empty when unknown (e.g., + /// in tests that construct commits manually). + pub files: Vec, } /// Try to parse each commit message as a [conventional commit](https://www.conventionalcommits.org/). diff --git a/crates/knope/src/config/package.rs b/crates/knope/src/config/package.rs index a7ef247d7..138825afa 100644 --- a/crates/knope/src/config/package.rs +++ b/crates/knope/src/config/package.rs @@ -61,6 +61,8 @@ pub struct Package { pub(crate) ignore_go_major_versioning: bool, pub(crate) update_internal_dependencies: InternalDependencyUpdate, pub(crate) internal_dependencies: Vec, + pub(crate) track_paths: bool, + pub(crate) paths: Vec, } impl Package { @@ -185,6 +187,8 @@ impl Package { ignore_go_major_versioning: false, update_internal_dependencies: InternalDependencyUpdate::default(), internal_dependencies: Vec::new(), + track_paths: false, + paths: Vec::new(), } }) .collect()) @@ -491,6 +495,8 @@ impl Package { ignore_go_major_versioning, update_internal_dependencies, internal_dependencies, + track_paths, + paths, } = package; let versioned_files = versioned_files .into_iter() @@ -526,6 +532,8 @@ impl Package { ignore_go_major_versioning, update_internal_dependencies, internal_dependencies, + track_paths, + paths, }) } } @@ -622,6 +630,8 @@ impl From for knope_config::Package { ignore_go_major_versioning: package.ignore_go_major_versioning, update_internal_dependencies: package.update_internal_dependencies, internal_dependencies: package.internal_dependencies, + track_paths: package.track_paths, + paths: package.paths, } } } diff --git a/crates/knope/src/integrations/git.rs b/crates/knope/src/integrations/git.rs index 4bcd0edd1..aa2651d17 100644 --- a/crates/knope/src/integrations/git.rs +++ b/crates/knope/src/integrations/git.rs @@ -407,9 +407,11 @@ pub(crate) fn get_commit_messages_after_tag(tag: Option<&str>) -> Result) -> Result Vec { + let Ok(new_tree) = commit.tree() else { + return Vec::new(); + }; + let parent_tree = commit.parent(0).ok().and_then(|parent| parent.tree().ok()); + let Ok(diff) = repo.diff_tree_to_tree(parent_tree.as_ref(), Some(&new_tree), None) else { + return Vec::new(); + }; + let mut files = Vec::new(); + diff.foreach( + &mut |delta, _| { + for path in [delta.new_file().path(), delta.old_file().path()] + .into_iter() + .flatten() + { + if let Some(s) = path.to_str() { + let rel = RelativePathBuf::from(s.replace('\\', "/")); + if !files.contains(&rel) { + files.push(rel); + } + } + } + true + }, + None, + None, + None, + ) + .ok(); + files +} + fn commit_info(commit: &git2::Commit) -> Option { Some(GitInfo { author_name: commit.author().name().map(String::from)?, diff --git a/crates/knope/src/step/releases/package.rs b/crates/knope/src/step/releases/package.rs index 5aeb89941..541d8e5e8 100644 --- a/crates/knope/src/step/releases/package.rs +++ b/crates/knope/src/step/releases/package.rs @@ -36,6 +36,12 @@ pub(crate) struct Package { /// Explicitly-declared names of packages this package depends on, supplementing the /// relationships derived from `versioned_files`. pub(crate) internal_dependencies: Vec, + /// When `true`, route conventional commits to this package by the files they touched. + pub(crate) track_paths: bool, + /// Directories/files that belong to this package for path-based commit filtering. When + /// `track_paths` is true and this is empty, parents of the package's `versioned_files` + /// are used. + pub(crate) paths: Vec, } impl Package { @@ -99,6 +105,8 @@ impl Package { override_version: None, update_internal_dependencies: package.update_internal_dependencies, internal_dependencies: package.internal_dependencies, + track_paths: package.track_paths, + paths: package.paths, }) } @@ -126,10 +134,23 @@ impl Package { let commit_messages = if should_ignore { Vec::new() } else { - conventional_commits::get_conventional_commits_after_last_stable_version( + let commits = conventional_commits::get_conventional_commits_after_last_stable_version( &self.versioning.name, all_tags, - )? + )?; + if self.track_paths { + let territory = self.territory(); + debug!( + "track_paths enabled for {pkg}; territory: {territory:?}", + pkg = self.versioning.name, + ); + commits + .into_iter() + .filter(|commit| commit_touches_any(commit, &territory)) + .collect() + } else { + commits + } }; let changeset_dir = PathBuf::from(CHANGESET_DIR); @@ -163,6 +184,30 @@ impl Package { Ok(self.versioning.get_changes(change_files, &commit_messages)) } + /// Compute the package's "territory" — the set of repo-relative directories/files used + /// for path-based commit filtering. Falls back to parent directories of non-dependency + /// `versioned_files` when no explicit `paths` were configured. + fn territory(&self) -> Vec { + if !self.paths.is_empty() { + return self.paths.clone(); + } + let mut out: Vec = Vec::new(); + for vf in self.versioning.versioned_files() { + if vf.dependency.is_some() { + continue; + } + let path = vf.as_path(); + let dir = path.parent().map_or_else( + || RelativePathBuf::from(""), + relative_path::RelativePath::to_relative_path_buf, + ); + if !out.contains(&dir) { + out.push(dir); + } + } + out + } + pub(crate) fn apply_release( &mut self, changes: &[Change], @@ -187,6 +232,35 @@ impl Package { } } +/// Returns `true` if any file changed by `commit` is inside any path in `territory`. An +/// empty territory matches no files (so the package's commits are effectively dropped — the +/// safe interpretation for "`track_paths` but nothing configured to track"). +fn commit_touches_any( + commit: &knope_versioning::changes::conventional_commit::Commit, + territory: &[RelativePathBuf], +) -> bool { + if territory.is_empty() { + return false; + } + commit + .files + .iter() + .any(|file| territory.iter().any(|root| path_is_within(file, root))) +} + +/// Whether `file` is `root` or sits inside `root` (treating `root` as a directory prefix). +fn path_is_within(file: &RelativePathBuf, root: &RelativePathBuf) -> bool { + if root.as_str().is_empty() { + return true; + } + if file == root { + return true; + } + let root_str = root.as_str().trim_end_matches('/'); + let file_str = file.as_str(); + file_str.starts_with(root_str) && file_str.as_bytes().get(root_str.len()).copied() == Some(b'/') +} + pub(crate) fn execute_prepare_actions( actions: RunType>, stage_to_git: bool, @@ -282,6 +356,8 @@ impl Package { go_versioning: GoVersioning::default(), update_internal_dependencies: InternalDependencyUpdate::default(), internal_dependencies: Vec::new(), + track_paths: false, + paths: Vec::new(), } } } diff --git a/crates/knope/tests/prepare_release/cargo/workspace/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/mod.rs index 927992afc..3e7707fe6 100644 --- a/crates/knope/tests/prepare_release/cargo/workspace/mod.rs +++ b/crates/knope/tests/prepare_release/cargo/workspace/mod.rs @@ -10,3 +10,7 @@ mod internal_dependency_updates_transitive; mod internal_dependency_updates_with_changelog; mod no_lock; mod shared_version; +mod track_paths_basic; +mod track_paths_explicit; +mod track_paths_multi; +mod track_paths_orphan; diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/Cargo.toml new file mode 100644 index 000000000..6efed9837 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/lib.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/lib.rs new file mode 100644 index 000000000..4094bb1d8 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/first/lib.rs @@ -0,0 +1 @@ +// Source file for the first package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/knope.toml new file mode 100644 index 000000000..348a205d4 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/knope.toml @@ -0,0 +1,13 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +track_paths = true + +[packages.second-package] +versioned_files = ["second/Cargo.toml"] +track_paths = true + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/lib.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/lib.rs new file mode 100644 index 000000000..2825a4be1 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/in/second/lib.rs @@ -0,0 +1 @@ +// Source file for the second package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/mod.rs new file mode 100644 index 000000000..8c83cc443 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/mod.rs @@ -0,0 +1,21 @@ +use crate::helpers::{ + GitCommand::{Add, Commit, Tag}, + TestCase, +}; + +/// With `track_paths` and no explicit `paths`, commits are routed to packages by the +/// parent directories of their versioned files. +#[test] +fn commits_route_by_versioned_file_directories() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Add("first/lib.rs"), + Commit("feat: A feature in first"), + Add("second/lib.rs"), + Commit("fix: A fix in second"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/first/Cargo.toml new file mode 100644 index 000000000..85dc7e92a --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_basic/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/docs/guide.md b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/docs/guide.md new file mode 100644 index 000000000..13070a9f0 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/docs/guide.md @@ -0,0 +1,3 @@ +# Guide + +Documentation owned by the first package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/first/Cargo.toml new file mode 100644 index 000000000..6efed9837 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/knope.toml new file mode 100644 index 000000000..fb3601a01 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/knope.toml @@ -0,0 +1,14 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +track_paths = true +paths = ["first", "docs"] + +[packages.second-package] +versioned_files = ["second/Cargo.toml"] +track_paths = true + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/lib.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/lib.rs new file mode 100644 index 000000000..2825a4be1 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/in/second/lib.rs @@ -0,0 +1 @@ +// Source file for the second package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/mod.rs new file mode 100644 index 000000000..b9dc89882 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/mod.rs @@ -0,0 +1,21 @@ +use crate::helpers::{ + GitCommand::{Add, Commit, Tag}, + TestCase, +}; + +/// Explicit `paths` override the versioned-file fallback: a commit touching `docs/` routes +/// to the first package because `docs` is in its configured paths. +#[test] +fn explicit_paths_override_versioned_file_directories() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Add("docs/guide.md"), + Commit("feat: A documented feature for first"), + Add("second/lib.rs"), + Commit("fix: A fix in second"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/first/Cargo.toml new file mode 100644 index 000000000..85dc7e92a --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_explicit/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/Cargo.toml new file mode 100644 index 000000000..6efed9837 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/lib.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/lib.rs new file mode 100644 index 000000000..4094bb1d8 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/first/lib.rs @@ -0,0 +1 @@ +// Source file for the first package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/knope.toml new file mode 100644 index 000000000..348a205d4 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/knope.toml @@ -0,0 +1,13 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +track_paths = true + +[packages.second-package] +versioned_files = ["second/Cargo.toml"] +track_paths = true + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/lib.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/lib.rs new file mode 100644 index 000000000..2825a4be1 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/in/second/lib.rs @@ -0,0 +1 @@ +// Source file for the second package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/mod.rs new file mode 100644 index 000000000..b8a7bc255 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/mod.rs @@ -0,0 +1,19 @@ +use crate::helpers::{ + GitCommand::{Add, Commit, Tag}, + TestCase, +}; + +/// A single commit touching files in multiple packages' territories applies to all of them. +#[test] +fn commit_touching_multiple_territories_applies_to_each() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Add("first/lib.rs"), + Add("second/lib.rs"), + Commit("feat: A cross-cutting feature"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/first/Cargo.toml new file mode 100644 index 000000000..85dc7e92a --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/second/Cargo.toml new file mode 100644 index 000000000..81c561431 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_multi/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.1" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/Cargo.toml new file mode 100644 index 000000000..b7939ff7b --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["first", "second"] diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/README.md b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/README.md new file mode 100644 index 000000000..392fb9448 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/README.md @@ -0,0 +1,3 @@ +# Workspace + +A file that belongs to no package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/Cargo.toml new file mode 100644 index 000000000..6efed9837 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.0.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/lib.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/lib.rs new file mode 100644 index 000000000..4094bb1d8 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/first/lib.rs @@ -0,0 +1 @@ +// Source file for the first package. diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/knope.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/knope.toml new file mode 100644 index 000000000..348a205d4 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/knope.toml @@ -0,0 +1,13 @@ +[packages.first-package] +versioned_files = ["first/Cargo.toml"] +track_paths = true + +[packages.second-package] +versioned_files = ["second/Cargo.toml"] +track_paths = true + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "PrepareRelease" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/in/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/mod.rs b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/mod.rs new file mode 100644 index 000000000..71ff4f2c2 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/mod.rs @@ -0,0 +1,22 @@ +use crate::helpers::{ + GitCommand::{Add, Commit, Tag}, + TestCase, +}; + +/// Commits touching only files outside every package's territory are dropped: the breaking +/// change to the README must not cause a major bump anywhere, and a package with no +/// matching commits is not released at all. +#[test] +fn commits_outside_all_territories_are_ignored() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("first-package/v1.0.0"), + Tag("second-package/v0.1.0"), + Add("first/lib.rs"), + Commit("feat: A feature in first"), + Add("README.md"), + Commit("fix!: A breaking change to repo infrastructure"), + ]) + .run("release"); +} diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/first/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/first/Cargo.toml new file mode 100644 index 000000000..85dc7e92a --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/first/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "first-package" +version = "1.1.0" diff --git a/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/second/Cargo.toml b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/second/Cargo.toml new file mode 100644 index 000000000..f2f748622 --- /dev/null +++ b/crates/knope/tests/prepare_release/cargo/workspace/track_paths_orphan/out/second/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "second-package" +version = "0.1.0" diff --git a/docs/src/content/docs/reference/Config File/packages.mdx b/docs/src/content/docs/reference/Config File/packages.mdx index d2c9f8258..20a271c95 100644 --- a/docs/src/content/docs/reference/Config File/packages.mdx +++ b/docs/src/content/docs/reference/Config File/packages.mdx @@ -567,3 +567,50 @@ versioned_files = ["crates/consumer/Cargo.toml"] update_internal_dependencies = "patch" internal_dependencies = ["my-lib"] ``` + +## `track_paths` + +By default, Knope routes conventional commits to packages using the commit +[`scopes`](#scopes) field. In a monorepo, that's friction: every developer has to remember +to add the right scope, and a missing or misspelled scope silently drops the change. + +Set `track_paths = true` on a package to route commits to it based on the **files each +commit touches** instead. A commit is included in this package's changes if any file it +modifies is inside the package's territory. + +A package's territory defaults to the parent directories of its own (non-dependency) +`versioned_files`. For a Cargo workspace member declared via `versioned_files = +["crates/my-crate/Cargo.toml"]`, the territory is `crates/my-crate/`. + +```toml title="knope.toml" +[packages.my-crate] +versioned_files = ["crates/my-crate/Cargo.toml"] +track_paths = true +``` + +## `paths` + +Override the auto-derived territory used by [`track_paths`](#track_paths). Useful when a +package's files live in multiple directories (e.g. shared utilities) or when the auto +default doesn't match your layout. + +```toml title="knope.toml" +[packages.my-crate] +versioned_files = ["crates/my-crate/Cargo.toml"] +track_paths = true +paths = ["crates/my-crate", "shared/api"] +``` + +### How `track_paths` interacts with the rest + +- **Commits in no package's territory are ignored.** A top-level `feat: README change` does + not bump any package, which is usually what you want. +- **A commit that touches files in multiple packages is credited to each of them.** Every + package whose territory was touched gets the change. +- **The `scopes` field is independent.** When `track_paths` is enabled, `scopes` is no longer + required for routing. If `scopes` is also set, scope filtering still applies: a commit that + has a scope must match the package's scopes _and_ the path filter; a commit with no scope + only needs to satisfy the path filter. +- **Internal dependency propagation still applies.** A package bumped by `track_paths` will + still cascade to its dependents via + [`update_internal_dependencies`](#update_internal_dependencies). From 70ed116f84734da295da65b2c1e2db381dd76b55 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 22:45:56 -0700 Subject: [PATCH 6/9] chore: add changesets Co-Authored-By: Claude Fable 5 --- ...route_commits_to_packages_by_paths_touched.md | 16 ++++++++++++++++ .changeset/track_paths_api_changes.md | 11 +++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .changeset/route_commits_to_packages_by_paths_touched.md create mode 100644 .changeset/track_paths_api_changes.md diff --git a/.changeset/route_commits_to_packages_by_paths_touched.md b/.changeset/route_commits_to_packages_by_paths_touched.md new file mode 100644 index 000000000..f64993d44 --- /dev/null +++ b/.changeset/route_commits_to_packages_by_paths_touched.md @@ -0,0 +1,16 @@ +--- +knope: minor +--- + +# Route commits to packages by the paths they touch with `track_paths` + +Setting `track_paths = true` on a package routes conventional commits to it based on the files each commit changed, instead of relying on commit scopes. By default, a package's territory is the parent directories of its own `versioned_files`; set `paths` to override it: + +```toml +[packages.my-package] +versioned_files = ["my-package/Cargo.toml"] +track_paths = true +paths = ["my-package", "docs/my-package"] # optional override +``` + +Commits that touch multiple packages' territories apply to each of them, and commits touching no tracked territory are ignored. See the [`track_paths` documentation](https://knope.tech/reference/config-file/packages#track_paths) for details. diff --git a/.changeset/track_paths_api_changes.md b/.changeset/track_paths_api_changes.md new file mode 100644 index 000000000..0386e4137 --- /dev/null +++ b/.changeset/track_paths_api_changes.md @@ -0,0 +1,11 @@ +--- +versioning: major +config: major +--- + +# New APIs for path-based commit routing + +To support `track_paths`: + +- `knope-versioning`: the conventional commit `Commit` struct has a new public `files` field listing the paths each commit changed (breaking for struct-literal construction). +- `knope-config`: `Package` has new `track_paths` and `paths` fields (breaking for struct-literal construction). From 2445170990c0366b1ba2216840a6416755ae3b52 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 22:30:59 -0700 Subject: [PATCH 7/9] feat: during pre-release only consider back to the last prerelease In prerelease mode, conventional commits are gathered since the most recent tag of any kind, not just the last stable. Iterative prerelease workflows (alpha.1 -> alpha.2) then only summarize the commits added since the previous prerelease, and packages with no new commits don't get re-released on every run. Because the delta may imply a lower stable target than an existing prerelease line (a fix following a feature that already shipped in rc.0), calculate_pre now continues the nearest existing prerelease line above the rule-derived version instead of regressing below a version that has already been pre-released. Rule-derived versions matching an existing line keep it, so deliberately-newer hand-tagged lines still don't take over. Co-Authored-By: Claude Fable 5 --- .../src/semver/package_versions.rs | 15 +++++++++++ .../src/step/releases/conventional_commits.rs | 25 ++++++++++++++++--- crates/knope/src/step/releases/package.rs | 3 ++- .../dryrun_stdout.log | 1 - crates/knope/tests/prepare_release/mod.rs | 1 + .../dryrun_stdout.log | 11 ++++++++ .../in/CHANGELOG.md | 5 ++++ .../in/Cargo.toml | 3 +++ .../in/knope.toml | 10 ++++++++ .../mod.rs | 21 ++++++++++++++++ .../out/CHANGELOG.md | 11 ++++++++ .../out/Cargo.toml | 3 +++ 12 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/dryrun_stdout.log create mode 100644 crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/CHANGELOG.md create mode 100644 crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/Cargo.toml create mode 100644 crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/knope.toml create mode 100644 crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/mod.rs create mode 100644 crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/CHANGELOG.md create mode 100644 crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/Cargo.toml diff --git a/crates/knope-versioning/src/semver/package_versions.rs b/crates/knope-versioning/src/semver/package_versions.rs index 7039d3906..b6b32c03c 100644 --- a/crates/knope-versioning/src/semver/package_versions.rs +++ b/crates/knope-versioning/src/semver/package_versions.rs @@ -171,6 +171,21 @@ impl PackageVersions { .map(|(stable, _)| *stable) .unwrap_or_default() }; + // An existing prerelease may already target a higher stable version (for example, a + // feature shipped in an earlier prerelease and the changes since then are only + // fixes). If the rule-derived version has no prerelease line of its own, continue + // the nearest existing line above it instead of regressing below a version that has + // already been pre-released. A rule-derived version that matches an existing line + // keeps it, so deliberately-newer lines (e.g. a hand-tagged 2.0.0-rc) don't take + // over (see `pre_after_same_pre`). + let stable_component = if self.prereleases.contains_key(&stable_component) { + stable_component + } else { + self.prereleases + .range(stable_component..) + .next() + .map_or(stable_component, |(existing, _)| *existing) + }; let pre_version = self .prereleases .get(&stable_component) diff --git a/crates/knope/src/step/releases/conventional_commits.rs b/crates/knope/src/step/releases/conventional_commits.rs index 3ac434e1a..68c8cbcd9 100644 --- a/crates/knope/src/step/releases/conventional_commits.rs +++ b/crates/knope/src/step/releases/conventional_commits.rs @@ -5,17 +5,34 @@ use tracing::debug; use crate::integrations::git::{self, get_commit_messages_after_tag}; -pub(crate) fn get_conventional_commits_after_last_stable_version( +/// Collect conventional commits from Git for a single package. +/// +/// In stable mode (`prerelease_mode = false`) we look back to the most recent **stable** +/// release tag, since prereleases are not user-facing releases. +/// +/// In prerelease mode (`prerelease_mode = true`) we look back to the most recent tag of +/// any kind — including prereleases. This lets iterative prerelease workflows (e.g., +/// alpha.1 → alpha.2) consider only the commits added since the previous prerelease, +/// rather than re-summarizing every commit since the last stable. +pub(crate) fn get_conventional_commits_after_last_release( package_name: &package::Name, all_tags: &[String], + prerelease_mode: bool, ) -> Result, git::Error> { debug!( "Getting conventional commits since last release of package {}", package_name.as_custom().unwrap_or_default() ); - let tag = PackageVersions::from_tags(package_name.as_custom(), all_tags) - .stable() - .map(|target_version| ReleaseTag::new(&target_version.into(), package_name)); + let versions = PackageVersions::from_tags(package_name.as_custom(), all_tags); + let tag = if prerelease_mode { + versions + .latest() + .map(|target_version| ReleaseTag::new(&target_version, package_name)) + } else { + versions + .stable() + .map(|target_version| ReleaseTag::new(&target_version.into(), package_name)) + }; get_commit_messages_after_tag(tag.as_ref().map(ReleaseTag::as_str)) } diff --git a/crates/knope/src/step/releases/package.rs b/crates/knope/src/step/releases/package.rs index 541d8e5e8..81f27fcb1 100644 --- a/crates/knope/src/step/releases/package.rs +++ b/crates/knope/src/step/releases/package.rs @@ -134,9 +134,10 @@ impl Package { let commit_messages = if should_ignore { Vec::new() } else { - let commits = conventional_commits::get_conventional_commits_after_last_stable_version( + let commits = conventional_commits::get_conventional_commits_after_last_release( &self.versioning.name, all_tags, + prepare_release.prerelease_label.is_some(), )?; if self.track_paths { let territory = self.territory(); diff --git a/crates/knope/tests/prepare_release/handle_pre_versions_that_are_too_new/dryrun_stdout.log b/crates/knope/tests/prepare_release/handle_pre_versions_that_are_too_new/dryrun_stdout.log index 84212e849..b34655234 100644 --- a/crates/knope/tests/prepare_release/handle_pre_versions_that_are_too_new/dryrun_stdout.log +++ b/crates/knope/tests/prepare_release/handle_pre_versions_that_are_too_new/dryrun_stdout.log @@ -4,7 +4,6 @@ Would add the following to CHANGELOG.md: ### Features -- A new feature - Another new feature Would add files to git: diff --git a/crates/knope/tests/prepare_release/mod.rs b/crates/knope/tests/prepare_release/mod.rs index baa370d25..d282d56f2 100644 --- a/crates/knope/tests/prepare_release/mod.rs +++ b/crates/knope/tests/prepare_release/mod.rs @@ -29,6 +29,7 @@ mod readme_version; mod release_after_prerelease; mod scopes; mod second_prerelease; +mod second_prerelease_after_distinct_stable; mod tauri_conf_json; mod unknown_versioned_file_format; mod verbose; diff --git a/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/dryrun_stdout.log b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/dryrun_stdout.log new file mode 100644 index 000000000..0f5bdcdaf --- /dev/null +++ b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/dryrun_stdout.log @@ -0,0 +1,11 @@ +Would add the following to Cargo.toml: version = 1.1.0-rc.1 +Would add the following to CHANGELOG.md: +## 1.1.0-rc.1 ([DATE]) + +### Fixes + +- A fix after the first RC + +Would add files to git: + Cargo.toml + CHANGELOG.md diff --git a/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/CHANGELOG.md b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/CHANGELOG.md new file mode 100644 index 000000000..11d97d61a --- /dev/null +++ b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.1.0-rc.0 + +### Features + +- New feature in first RC diff --git a/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/Cargo.toml b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/Cargo.toml new file mode 100644 index 000000000..9bac9dae3 --- /dev/null +++ b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "default" +version = "1.1.0-rc.0" diff --git a/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/knope.toml b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/knope.toml new file mode 100644 index 000000000..b06f62c8e --- /dev/null +++ b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/in/knope.toml @@ -0,0 +1,10 @@ +[package] +versioned_files = ["Cargo.toml"] +changelog = "CHANGELOG.md" + +[[workflows]] +name = "prerelease" + +[[workflows.steps]] +type = "PrepareRelease" +prerelease_label = "rc" diff --git a/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/mod.rs b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/mod.rs new file mode 100644 index 000000000..d99daeaa4 --- /dev/null +++ b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/mod.rs @@ -0,0 +1,21 @@ +use crate::helpers::{ + GitCommand::{Commit, Tag}, + TestCase, +}; + +/// A second prerelease where the commits since the last prerelease imply a _lower_ stable +/// target (a fix) than the existing prerelease line (which shipped a feature). The new +/// prerelease must continue the existing line (1.1.0-rc.1), not regress to 1.0.1-rc.0, +/// and its changelog must only contain the new commits. +#[test] +fn test() { + TestCase::new(file!()) + .git(&[ + Commit("Initial commit"), + Tag("v1.0.0"), + Commit("feat: New feature in first RC"), + Tag("v1.1.0-rc.0"), + Commit("fix: A fix after the first RC"), + ]) + .run("prerelease"); +} diff --git a/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/CHANGELOG.md b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/CHANGELOG.md new file mode 100644 index 000000000..a982a4384 --- /dev/null +++ b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/CHANGELOG.md @@ -0,0 +1,11 @@ +## 1.1.0-rc.1 ([DATE]) + +### Fixes + +- A fix after the first RC + +## 1.1.0-rc.0 + +### Features + +- New feature in first RC diff --git a/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/Cargo.toml b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/Cargo.toml new file mode 100644 index 000000000..94216a5f7 --- /dev/null +++ b/crates/knope/tests/prepare_release/second_prerelease_after_distinct_stable/out/Cargo.toml @@ -0,0 +1,3 @@ +[package] +name = "default" +version = "1.1.0-rc.1" From a749133568e17f6606ded63300476e9deb6cd224 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 11 Jun 2026 22:46:12 -0700 Subject: [PATCH 8/9] chore: add changeset Co-Authored-By: Claude Fable 5 --- ...only_contain_changes_since_the_last_prerelease.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/prerelease_changelogs_only_contain_changes_since_the_last_prerelease.md diff --git a/.changeset/prerelease_changelogs_only_contain_changes_since_the_last_prerelease.md b/.changeset/prerelease_changelogs_only_contain_changes_since_the_last_prerelease.md new file mode 100644 index 000000000..5e9da6b6c --- /dev/null +++ b/.changeset/prerelease_changelogs_only_contain_changes_since_the_last_prerelease.md @@ -0,0 +1,12 @@ +--- +knope: major +versioning: major +--- + +# Prerelease changelogs only contain changes since the last prerelease + +When running `PrepareRelease` with a prerelease label, Knope now collects conventional commits since the most recent tag of _any_ kind—including prereleases—instead of the last stable release. Iterative prerelease workflows (`alpha.1` → `alpha.2`) get changelogs containing only the commits added since the previous prerelease, and packages with no new commits are no longer re-released on every run. + +If the commits since the last prerelease imply a lower version than an existing prerelease line (for example, a fix following a feature that already shipped in `1.1.0-rc.0`), the new version continues the existing line (`1.1.0-rc.1`) rather than regressing below a version that has already been pre-released. This also applies to `knope-versioning`'s version calculation: a rule-derived version with no prerelease line of its own continues the nearest existing line above it. + +Stable releases are unaffected: they still summarize everything since the last stable release. From bc3f24426879d22d93f844c893457697b816e280 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 17 Jun 2026 15:39:07 -0700 Subject: [PATCH 9/9] docs: make new docs prose Vale-clean The Vale CI check lints the docs tree through reviewdog and annotates every finding on a PR's added lines. Once those exceed reviewdog's annotation cap it fails the job with "Too many results (annotations) in diff", which is what the new docs sections triggered (mostly passive voice and long-sentence suggestions, plus a few error-level findings). Rewrite the added prose to be fully Vale-clean: active voice, shorter sentences, no spaced em-dashes / "e.g." / uncontracted negatives, no "multiple"/"silently"/semicolons, and the accepted "Changeset" term. Verified locally with Vale 3.15.1 and the repo's config: zero findings of any severity on the added lines. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../docs/recipes/updating-dependencies.md | 24 ++++---- .../docs/reference/Config File/packages.mdx | 57 ++++++++++--------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/docs/src/content/docs/recipes/updating-dependencies.md b/docs/src/content/docs/recipes/updating-dependencies.md index ad9d0f045..1ba866286 100644 --- a/docs/src/content/docs/recipes/updating-dependencies.md +++ b/docs/src/content/docs/recipes/updating-dependencies.md @@ -69,28 +69,28 @@ You can use this same `dependency` field to override the name of a package updat ## Releasing dependents automatically Wiring up `versioned_files` keeps the dependency _string_ inside dependent packages in sync, -but by default each dependent must still have its own [changeset](/reference/concepts/change) +but by default each dependent must still have its own [Changeset](/reference/concepts/change) to actually trigger a release of that dependent. To release dependents automatically whenever one of their internal dependencies releases, set -`update_internal_dependencies` on the dependent's package config to `"patch"` or `"minor"` — -see the +`update_internal_dependencies` on the dependent's package config to `"patch"` or `"minor"`. +See the [`update_internal_dependencies` reference](/reference/config-file/packages#update_internal_dependencies) -for details. The default is `"none"`: dependency strings are kept in sync, but no release -is created for the dependent. +for details. The default is `"none"`: Knope keeps dependency strings in sync but creates no +release for the dependent. -For example, given the setup above where `knope` depends on `knope-versioning`, setting -`update_internal_dependencies = "patch"` on the `knope` package means releasing -`knope-versioning` will also release `knope` as a patch, and the new release notes for -`knope` will include a `Dependencies` section listing the bumped dependencies. +For example, take the earlier setup where `knope` depends on `knope-versioning`. Set +`update_internal_dependencies = "patch"` on the `knope` package. Now releasing +`knope-versioning` also releases `knope` as a patch, and the new `knope` release notes +include a `Dependencies` section that lists the bumped dependencies. :::caution Knope finds the dependency relationships by reading each opted-in package's manifests (`Cargo.toml`, `package.json`) and from `versioned_files` entries that point at another -package's files. If a relationship isn't visible to either — for example, the version is -only tracked in the workspace-root `Cargo.toml` via `[workspace.dependencies]` — declare it -explicitly with +package's files. Sometimes a relationship isn't visible to either. For example, you might +track the version only in the workspace-root `Cargo.toml` via `[workspace.dependencies]`. In +those cases, declare it explicitly with [`internal_dependencies`](/reference/config-file/packages#internal_dependencies) on the dependent's package config. diff --git a/docs/src/content/docs/reference/Config File/packages.mdx b/docs/src/content/docs/reference/Config File/packages.mdx index 20a271c95..433160ad1 100644 --- a/docs/src/content/docs/reference/Config File/packages.mdx +++ b/docs/src/content/docs/reference/Config File/packages.mdx @@ -490,13 +490,13 @@ To maintain multiple major versions of a Go module, check out [this recipe](/rec ## `update_internal_dependencies` When a package is part of a monorepo and depends on another package in the same repo, -Knope can automatically release the dependent whenever the dependency is released. +Knope can automatically release the dependent whenever the dependency releases. Set `update_internal_dependencies` on the dependent's package config to control the bump: -- `"none"` (default): do not release the dependent when its dependency changes. - The dependency's new version is still written into the dependent's files (so the build - stays consistent), but no version bump or release notes are produced for the dependent. +- `"none"` (default): don't release the dependent when its dependency changes. Knope still + writes the dependency's new version into the dependent's files (keeping the build + consistent), but creates no version bump or release notes for the dependent. - `"patch"`: release the dependent as a patch bump. - `"minor"`: release the dependent as a minor bump. @@ -508,7 +508,7 @@ update_internal_dependencies = "patch" # or "minor"; defaults to "none" When this propagation triggers a release, Knope adds a "Dependencies" section to the dependent's release notes, in the style of the -[changesets](https://github.com/changesets/changesets) tool: +[Changeset](https://github.com/changesets/changesets) tool: ```markdown ## 1.0.1 (2026-06-04) @@ -525,26 +525,27 @@ will bump `B`, which will in turn bump `A` (subject to each package's own ### How Knope finds dependents -For any package that opted in, Knope reads its manifests (currently `Cargo.toml` and -`package.json`): if a manifest declares a dependency whose name matches another package's -manifest name, the relationship is detected automatically — no extra configuration needed. -Note that this alone doesn't keep the dependency's version string inside the manifest up to -date; add a `{ path, dependency }` entry to the dependency's `versioned_files` if you want -that written on every release. +For any package that opts in, Knope reads its manifests (currently `Cargo.toml` and +`package.json`). If a manifest declares a dependency whose name matches another package's +manifest name, Knope detects the relationship with no extra configuration. + +This detection alone doesn't keep the dependency's version string inside the manifest +current. To write that on every release, add a `{ path, dependency }` entry to the +dependency's `versioned_files`. Those `{ path, dependency }` entries also imply relationships: an entry like `{ path = "consumer/Cargo.toml", dependency = "my-lib" }` in the `my-lib` package's config means "when `my-lib` releases, write its new version into `consumer/Cargo.toml`". When that file is also a versioned file of another package (`consumer` here), Knope records that -`consumer` depends on `my-lib` — the name the dependency goes by _inside_ the file doesn't +`consumer` depends on `my-lib`. The name the dependency goes by _inside_ the file doesn't need to match its package name in `knope.toml`. Lock files (like `Cargo.lock` or `package-lock.json`) record versions for the whole workspace, so sharing one does **not** create a dependency relationship. -If a relationship isn't visible to either mechanism — for example, the manifest format isn't -supported yet, or the version is only tracked in a shared workspace manifest via -`[workspace.dependencies]` — declare it explicitly with +Sometimes a relationship isn't visible to either mechanism. Knope might not support the +manifest format yet, or you might track the version only in a shared workspace manifest via +`[workspace.dependencies]`. In those cases, declare it explicitly with [`internal_dependencies`](#internal_dependencies). ## `internal_dependencies` @@ -572,11 +573,11 @@ internal_dependencies = ["my-lib"] By default, Knope routes conventional commits to packages using the commit [`scopes`](#scopes) field. In a monorepo, that's friction: every developer has to remember -to add the right scope, and a missing or misspelled scope silently drops the change. +to add the right scope, and a missing or misspelled scope drops the change. Set `track_paths = true` on a package to route commits to it based on the **files each -commit touches** instead. A commit is included in this package's changes if any file it -modifies is inside the package's territory. +commit touches** instead. A commit counts toward this package's changes when it modifies a +file inside the package's territory. A package's territory defaults to the parent directories of its own (non-dependency) `versioned_files`. For a Cargo workspace member declared via `versioned_files = @@ -590,8 +591,8 @@ track_paths = true ## `paths` -Override the auto-derived territory used by [`track_paths`](#track_paths). Useful when a -package's files live in multiple directories (e.g. shared utilities) or when the auto +Override the territory that [`track_paths`](#track_paths) derives by default. This helps when +a package's files live in several directories (for example, shared utilities) or when the default doesn't match your layout. ```toml title="knope.toml" @@ -603,14 +604,14 @@ paths = ["crates/my-crate", "shared/api"] ### How `track_paths` interacts with the rest -- **Commits in no package's territory are ignored.** A top-level `feat: README change` does - not bump any package, which is usually what you want. -- **A commit that touches files in multiple packages is credited to each of them.** Every - package whose territory was touched gets the change. -- **The `scopes` field is independent.** When `track_paths` is enabled, `scopes` is no longer - required for routing. If `scopes` is also set, scope filtering still applies: a commit that - has a scope must match the package's scopes _and_ the path filter; a commit with no scope - only needs to satisfy the path filter. +- **Knope ignores commits in no package's territory.** A top-level `feat: README change` + doesn't bump any package, which is usually what you want. +- **A commit that touches files in several packages applies to each of them.** Every package + whose territory the commit touches gets the change. +- **The `scopes` field stays independent.** With `track_paths` on, routing no longer needs + `scopes`. If you also set `scopes`, scope filtering still applies. A commit that has a scope + must match both the package's scopes _and_ the path filter. A commit with no scope only + needs to satisfy the path filter. - **Internal dependency propagation still applies.** A package bumped by `track_paths` will still cascade to its dependents via [`update_internal_dependencies`](#update_internal_dependencies).