From d00a2f0a570ef320d5f2f87bc806ba2f01365f5f Mon Sep 17 00:00:00 2001 From: James McMahon Date: Wed, 28 Oct 2026 17:40:34 +0000 Subject: [PATCH] feat: bump packages for root shared file changes --- .../SemVer/DirectoryBumpCalculatorTests.cs | 91 +++++++++++++++++++ .../SemVer/DirectoryBumpCalculator.cs | 19 +++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs b/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs index fe4ef8f..29748a6 100644 --- a/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs +++ b/src/PackageCascade.Tests.Unit/Internal/Planning/SemVer/DirectoryBumpCalculatorTests.cs @@ -7,6 +7,97 @@ namespace PackageCascade.Tests.Unit.Internal.Planning.SemVer; public class DirectoryBumpCalculatorTests { + [Test] + [Arguments("Directory.Build.props")] + [Arguments("Directory.Build.targets")] + [Arguments("Directory.Packages.props")] + [Arguments("global.json")] + [Arguments("NuGet.Config")] + public async Task RootSharedFileChangeBumpsEveryDirectory(string path) + { + GitCommitDetails commit = new("commit", "build: update shared settings", [path]); + + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "tools/B"]); + + using (Assert.Multiple()) + { + await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.Patch); + await Assert.That(bumps["tools/B"]).IsEqualTo(SemverBump.Patch); + } + } + + [Test] + [Arguments("Update shared settings", "Patch")] + [Arguments("fix: correct shared settings", "Patch")] + [Arguments("feat: enable new build feature", "Minor")] + [Arguments("build!: change shared settings", "Major")] + public async Task RootSharedFileUsesCommitBumpWithPatchMinimum(string message, string expected) + { + GitCommitDetails commit = new("commit", message, ["Directory.Build.props"]); + + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "src/B"]); + + using (Assert.Multiple()) + { + await Assert.That(bumps["src/A"].ToString()).IsEqualTo(expected); + await Assert.That(bumps["src/B"].ToString()).IsEqualTo(expected); + } + } + + [Test] + [Arguments("src/A/Directory.Build.props")] + [Arguments("src/A/Directory.Build.targets")] + [Arguments("src/A/Directory.Packages.props")] + [Arguments("src/A/global.json")] + [Arguments("src/A/NuGet.Config")] + public async Task NestedSharedFilenameOnlyAffectsItsOwnDirectory(string path) + { + GitCommitDetails commit = new("commit", "fix: correct settings", [path]); + + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A", "src/B"]); + + using (Assert.Multiple()) + { + await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.Patch); + await Assert.That(bumps["src/B"]).IsEqualTo(SemverBump.None); + } + } + + [Test] + [Arguments("README.md")] + [Arguments(".editorconfig")] + [Arguments("Example.slnx")] + [Arguments("Directory.Build.props.bak")] + public async Task OtherRootFilesDoNotBumpEveryDirectory(string path) + { + GitCommitDetails commit = new("commit", "fix: update repository file", [path]); + + Dictionary bumps = new DirectoryBumpCalculator().Calculate([commit], ["src/A"]); + + await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.None); + } + + [Test] + [Arguments(false)] + [Arguments(true)] + public async Task CombinesSharedAndPackageChangesUsingStrongestBump(bool reverseOrder) + { + GitCommitDetails[] commits = + [ + new("shared", "feat: enable shared feature", ["global.json"]), + new("package", "feat!: change A API", ["src/A/Api.cs"]), + ]; + + Dictionary bumps = new DirectoryBumpCalculator().Calculate( + reverseOrder ? commits.Reverse() : commits, ["src/A", "src/B"]); + + using (Assert.Multiple()) + { + await Assert.That(bumps["src/A"]).IsEqualTo(SemverBump.Major); + await Assert.That(bumps["src/B"]).IsEqualTo(SemverBump.Minor); + } + } + [Test] [Arguments("fix: repair", "Patch")] [Arguments("perf(core): speed up", "Patch")] diff --git a/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs b/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs index 4722356..b484d74 100644 --- a/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs +++ b/src/PackageCascade/Internal/Planning/SemVer/DirectoryBumpCalculator.cs @@ -20,23 +20,29 @@ public Dictionary Calculate(IEnumerable co foreach (GitCommitDetails commit in commits) { SemverBump bump = GetBump(commit.Message); + bool changesRootSharedFile = commit.ChangedPaths.Any(IsRootSharedFile); + if (changesRootSharedFile && bump < SemverBump.Patch) + { + bump = SemverBump.Patch; + } + if (bump == SemverBump.None) { continue; } - ApplyBumpToAffectedDirectories(commit, bump, directoryPrefixes, bumps); + ApplyBumpToAffectedDirectories(commit, bump, changesRootSharedFile, directoryPrefixes, bumps); } return bumps; } - private static void ApplyBumpToAffectedDirectories(GitCommitDetails commit, SemverBump bump, + private static void ApplyBumpToAffectedDirectories(GitCommitDetails commit, SemverBump bump, bool changesRootSharedFile, IReadOnlyDictionary directoryPrefixes, Dictionary bumps) { foreach ((string directory, string prefix) in directoryPrefixes) { - if (commit.ChangedPaths.Any(path => path.StartsWith(prefix, StringComparison.Ordinal)) + if ((changesRootSharedFile || commit.ChangedPaths.Any(path => path.StartsWith(prefix, StringComparison.Ordinal))) && bump > bumps[directory]) { bumps[directory] = bump; @@ -44,6 +50,13 @@ private static void ApplyBumpToAffectedDirectories(GitCommitDetails commit, Semv } } + private static bool IsRootSharedFile(string path) => path is + "Directory.Build.props" or + "Directory.Build.targets" or + "Directory.Packages.props" or + "global.json" or + "NuGet.Config"; + private static SemverBump GetBump(string message) { Match header = HeaderPattern().Match(message);