diff --git a/src/PackageCascade.Cli.Tests.Unit/Serialization/PlanJsonOptionsTests.cs b/src/PackageCascade.Cli.Tests.Unit/Serialization/PlanJsonOptionsTests.cs index a85b0ed..1674bfb 100644 --- a/src/PackageCascade.Cli.Tests.Unit/Serialization/PlanJsonOptionsTests.cs +++ b/src/PackageCascade.Cli.Tests.Unit/Serialization/PlanJsonOptionsTests.cs @@ -14,7 +14,7 @@ [new PlannedRelease("Fixture", "src/Fixture/Fixture.csproj", "1.2.4-beta.1", [new ConventionalCommitReason(SemverBump.Minor, ["abc"]), new DependencyReleaseReason("Foundation")], ["Foundation"]), new PlannedRelease("NewPackage", "src/NewPackage/NewPackage.csproj", "0.1.0", - [new InitialReleaseReason()], [])], "target-sha", "Release", + [new InitialReleaseReason()], [])], "Release", [new ReferencedPackage("Foundation", "src/Foundation/Foundation.csproj", "3.2.1")], "solutions"); string json = JsonSerializer.Serialize(plan, PlanJsonOptions.Options); @@ -30,7 +30,6 @@ [new InitialReleaseReason()], [])], "target-sha", "Release", await Assert.That(package.PackageId).IsEqualTo("Fixture"); await Assert.That(copy.Packages[1].Reasons.Single() is InitialReleaseReason).IsTrue(); await Assert.That(copy.Packages[1].Version).IsEqualTo("0.1.0"); - await Assert.That(copy.TargetCommitSha).IsEqualTo("target-sha"); await Assert.That(copy.Configuration).IsEqualTo("Release"); await Assert.That(copy.WorkingDirectory).IsEqualTo("solutions"); await Assert.That(package.DependencyIds).IsEquivalentTo(["Foundation"]); diff --git a/src/PackageCascade.Tests.Integration/CliOnly/PlanCommandTests.cs b/src/PackageCascade.Tests.Integration/CliOnly/PlanCommandTests.cs index a14d6f0..e57042b 100644 --- a/src/PackageCascade.Tests.Integration/CliOnly/PlanCommandTests.cs +++ b/src/PackageCascade.Tests.Integration/CliOnly/PlanCommandTests.cs @@ -68,7 +68,7 @@ public async Task PlanWritesReadableVersionsAndReasonTypes() ReleasePlan plan = new( [new PlannedRelease("Fixture", "src/Fixture/Fixture.csproj", "1.2.4", [new ConventionalCommitReason(SemverBump.Patch, ["abc"]), - new DependencyReleaseReason("Foundation")], ["Foundation"])], "target-sha", "Release", + new DependencyReleaseReason("Foundation")], ["Foundation"])], "Release", [new ReferencedPackage("Foundation", "src/Foundation/Foundation.csproj", "3.2.1")], "."); StubPlanner planner = new(plan); TestBootstrap bootstrap = new(services => services.AddSingleton(planner)); @@ -108,7 +108,7 @@ public ReleasePlan Generate(string repositoryPath, string solutionPath, string c SolutionPath = solutionPath; Configuration = configuration; - return _plan ?? new([], "target-sha", configuration, [], "."); + return _plan ?? new([], configuration, [], "."); } } } diff --git a/src/PackageCascade.Tests.Integration/CoreOnly/Planning/PlannerIntegrationTests.cs b/src/PackageCascade.Tests.Integration/CoreOnly/Planning/PlannerIntegrationTests.cs index 534ef79..9295772 100644 --- a/src/PackageCascade.Tests.Integration/CoreOnly/Planning/PlannerIntegrationTests.cs +++ b/src/PackageCascade.Tests.Integration/CoreOnly/Planning/PlannerIntegrationTests.cs @@ -49,7 +49,6 @@ await File.WriteAllTextAsync(Path.Combine(projectDirectory, "Fixture.csproj"), " using (Assert.Multiple()) { - await Assert.That(plan.TargetCommitSha).IsEqualTo(_repository.Head.Tip!.Sha); await Assert.That(plan.Configuration).IsEqualTo("Release"); await Assert.That(release.PackageId).IsEqualTo("Fixture"); await Assert.That(release.Version).IsEqualTo("1.2.4"); diff --git a/src/PackageCascade.Tests.Unit/Internal/Executing/Git/PackageTagWriterTests.cs b/src/PackageCascade.Tests.Unit/Internal/Executing/Git/PackageTagWriterTests.cs index 24ab5c0..92d717e 100644 --- a/src/PackageCascade.Tests.Unit/Internal/Executing/Git/PackageTagWriterTests.cs +++ b/src/PackageCascade.Tests.Unit/Internal/Executing/Git/PackageTagWriterTests.cs @@ -17,19 +17,19 @@ public PackageTagWriterTests() } [Test] - public async Task CreatesLightweightTagsAtPlanCommitAfterHeadMoves() + public async Task CreatesLightweightTagsAtCurrentHead() { - Commit target = Commit("target"); - Commit("new head"); + Commit("target"); + Commit head = Commit("new head"); - IReadOnlyList created = _writer.Create(_directory, Plan(target, ("Acme.Core", "1.2.3"), ("Acme.Tools", "2.0.0"))); + IReadOnlyList created = _writer.Create(_directory, Plan(("Acme.Core", "1.2.3"), ("Acme.Tools", "2.0.0"))); using (Assert.Multiple()) { await Assert.That(created).IsEquivalentTo(new[] { "Acme.Core-v1.2.3", "Acme.Tools-v2.0.0" }); await Assert.That(_repository.Tags["Acme.Core-v1.2.3"].Target).IsTypeOf(); - await Assert.That(_repository.Tags["Acme.Core-v1.2.3"].PeeledTarget.Id).IsEqualTo(target.Id); - await Assert.That(_repository.Tags["Acme.Tools-v2.0.0"].PeeledTarget.Id).IsEqualTo(target.Id); + await Assert.That(_repository.Tags["Acme.Core-v1.2.3"].PeeledTarget.Id).IsEqualTo(head.Id); + await Assert.That(_repository.Tags["Acme.Tools-v2.0.0"].PeeledTarget.Id).IsEqualTo(head.Id); } } @@ -37,7 +37,7 @@ public async Task CreatesLightweightTagsAtPlanCommitAfterHeadMoves() public async Task RepeatedCreateThrowsAndKeepsFirstTag() { Commit target = Commit("target"); - ReleasePlan plan = Plan(target, ("Acme.Core", "1.0.0")); + ReleasePlan plan = Plan(("Acme.Core", "1.0.0")); _writer.Create(_directory, plan); Assert.Throws(typeof(LibGit2SharpException), () => _writer.Create(_directory, plan)); @@ -53,7 +53,7 @@ public async Task ConflictingSecondTagLeavesFirstTag() _repository.Tags.Add("Acme.Tools-v2.0.0", other); Assert.Throws(typeof(LibGit2SharpException), () => _writer.Create(_directory, - Plan(target, ("Acme.Core", "1.0.0"), ("Acme.Tools", "2.0.0")))); + Plan(("Acme.Core", "1.0.0"), ("Acme.Tools", "2.0.0")))); using (Assert.Multiple()) { @@ -68,7 +68,7 @@ public async Task InvalidSecondTagLeavesFirstTag() Commit target = Commit("target"); Assert.Throws(typeof(LibGit2SharpException), () => _writer.Create(_directory, - Plan(target, ("Acme.Core", "1.0.0"), ("Bad..Name", "2.0.0")))); + Plan(("Acme.Core", "1.0.0"), ("Bad..Name", "2.0.0")))); await Assert.That(_repository.Tags["Acme.Core-v1.0.0"].PeeledTarget.Id).IsEqualTo(target.Id); } @@ -81,7 +81,7 @@ public async Task TagWriteFailureLeavesEarlierTag() File.WriteAllText(lockPath, "held"); Assert.Throws(typeof(LibGit2SharpException), () => _writer.Create(_directory, - Plan(target, ("Acme.Core", "1.0.0"), ("Acme.Tools", "2.0.0")))); + Plan(("Acme.Core", "1.0.0"), ("Acme.Tools", "2.0.0")))); using (Assert.Multiple()) { @@ -104,6 +104,6 @@ private Commit Commit(string contents) return _repository.Commit(contents, author, author); } - private static ReleasePlan Plan(Commit target, params (string PackageId, string Version)[] packages) - => new(packages.Select(package => new PlannedRelease(package.PackageId, $"{package.PackageId}.csproj", package.Version, [], [])).ToArray(), target.Sha, "Release", [], "."); + private static ReleasePlan Plan(params (string PackageId, string Version)[] packages) + => new(packages.Select(package => new PlannedRelease(package.PackageId, $"{package.PackageId}.csproj", package.Version, [], [])).ToArray(), "Release", [], "."); } diff --git a/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageArchiveVerifierTests.cs b/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageArchiveVerifierTests.cs index e03ade3..10db01e 100644 --- a/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageArchiveVerifierTests.cs +++ b/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageArchiveVerifierTests.cs @@ -9,7 +9,7 @@ public sealed class PackageArchiveVerifierTests : IDisposable private readonly PackageArchiveVerifier _verifier = new(); [Test] - public void AcceptsNamespacedGroupedDependenciesAndSymbols() + public async Task AcceptsNamespacedGroupedDependenciesAndSymbols() { string grouped = Archive("A.nupkg", Manifest("A", "1.2.3", """ @@ -21,56 +21,65 @@ public void AcceptsNamespacedGroupedDependenciesAndSymbols() string symbolsB = Archive("B.snupkg", Manifest("B", "2.0.0")); ExpectedPackage a = Expected("A", "1.2.3", new Dictionary { ["B"] = "2.0.0" }); - _verifier.Verify([grouped, ungrouped, symbolsA, symbolsB], [a, Expected("B", "2.0.0")], includeSymbols: true); + await _verifier.Verify([grouped, ungrouped, symbolsA, symbolsB], [a, Expected("B", "2.0.0")], includeSymbols: true); } [Test] [Arguments("2.0.0")] [Arguments("[2.0.0]")] [Arguments("[2.0.0,3.0.0)")] - public void AcceptsUngroupedDependencyWithPlannedMinimum(string range) + public async Task AcceptsUngroupedDependencyWithPlannedMinimum(string range) { string archive = Archive("A.nupkg", Manifest("A", "1.2.3", $"")); - _verifier.Verify([archive], [Expected("A", "1.2.3", new Dictionary { ["B"] = "2.0.0" })], false); + await _verifier.Verify([archive], [Expected("A", "1.2.3", new Dictionary { ["B"] = "2.0.0" })], false); } [Test] - public void RejectsMissingPackageArchive() + public async Task RejectsMissingPackageArchive() { - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsDuplicatePackageArchive() + public async Task RejectsCancelledVerification() + { + using CancellationTokenSource cancellation = new(); + await cancellation.CancelAsync(); + await Assert.ThrowsAsync(() => + _verifier.Verify([], [], false, cancellation.Token)); + } + + [Test] + public async Task RejectsDuplicatePackageArchive() { string a = Archive("A.nupkg", Manifest("A", "1.0.0")); string duplicate = Archive("copy.nupkg", Manifest("A", "1.0.0")); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([a, duplicate], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([a, duplicate], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsUnexpectedPackageArchive() + public async Task RejectsUnexpectedPackageArchive() { string a = Archive("A.nupkg", Manifest("A", "1.0.0")); string unexpected = Archive("other.nupkg", Manifest("Other", "1.0.0")); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([a, unexpected], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([a, unexpected], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsMissingSymbolArchive() + public async Task RejectsMissingSymbolArchive() { string a = Archive("A.nupkg", Manifest("A", "1.0.0")); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([a], [Expected("A", "1.0.0")], true)); + await Assert.ThrowsAsync(() => _verifier.Verify([a], [Expected("A", "1.0.0")], true)); } [Test] - public void RejectsDuplicateSymbolArchive() + public async Task RejectsDuplicateSymbolArchive() { string a = Archive("A.nupkg", Manifest("A", "1.0.0")); string symbols = Archive("A.snupkg", Manifest("A", "1.0.0")); string duplicateSymbols = Archive("copy.snupkg", Manifest("A", "1.0.0")); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([a, symbols, duplicateSymbols], [Expected("A", "1.0.0")], true)); + await Assert.ThrowsAsync(() => _verifier.Verify([a, symbols, duplicateSymbols], [Expected("A", "1.0.0")], true)); } [Test] @@ -78,72 +87,72 @@ public void RejectsDuplicateSymbolArchive() [Arguments(false, "A", "2.0.0")] [Arguments(true, "B", "1.0.0")] [Arguments(true, "A", "2.0.0")] - public void RejectsWrongIdentityOrVersion(bool isSymbol, string archiveId, string archiveVersion) + public async Task RejectsWrongIdentityOrVersion(bool isSymbol, string archiveId, string archiveVersion) { string archive = Archive(isSymbol ? "A.snupkg" : "A.nupkg", Manifest(archiveId, archiveVersion)); string[] archives = isSymbol ? [Archive("A.nupkg", Manifest("A", "1.0.0")), archive] : [archive]; - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify(archives, [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify(archives, [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsCorruptArchive() + public async Task RejectsCorruptArchive() { string corrupt = Path.Combine(_directory, "corrupt.nupkg"); File.WriteAllText(corrupt, "not a zip"); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([corrupt], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([corrupt], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsArchiveWithoutManifest() + public async Task RejectsArchiveWithoutManifest() { string noManifest = Archive("empty.nupkg"); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([noManifest], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([noManifest], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsNestedManifest() + public async Task RejectsNestedManifest() { string nested = Archive("nested.nupkg", Manifest("A", "1.0.0"), "nested/A.nuspec"); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([nested], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([nested], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsMultipleRootManifests() + public async Task RejectsMultipleRootManifests() { string twoManifests = Archive("two.nupkg", Manifest("A", "1.0.0"), "A.nuspec", "B.nuspec"); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([twoManifests], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([twoManifests], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsMalformedManifest() + public async Task RejectsMalformedManifest() { string malformed = Archive("malformed.nupkg", ""); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([malformed], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([malformed], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsManifestWithDtd() + public async Task RejectsManifestWithDtd() { string dtd = Archive("dtd.nupkg", "]>&x;1.0.0"); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([dtd], [Expected("A", "1.0.0")], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([dtd], [Expected("A", "1.0.0")], false)); } [Test] - public void RejectsMissingDependency() + public async Task RejectsMissingDependency() { ExpectedPackage package = Expected("A", "1.0.0", new Dictionary { ["B"] = "2.0.0" }); string missing = Archive("missing-dependency.nupkg", Manifest("A", "1.0.0")); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([missing], [package], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([missing], [package], false)); } [Test] - public void RejectsStaleDependencyOccurrenceInAnotherGroup() + public async Task RejectsStaleDependencyOccurrenceInAnotherGroup() { ExpectedPackage package = Expected("A", "1.0.0", new Dictionary { ["B"] = "2.0.0" }); string stale = Archive("stale-dependency.nupkg", Manifest("A", "1.0.0", """ """)); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([stale], [package], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([stale], [package], false)); } [Test] @@ -152,12 +161,12 @@ public void RejectsStaleDependencyOccurrenceInAnotherGroup() [Arguments("[3.0.0,)")] [Arguments("not-a-range")] [Arguments("2.*")] - public void RejectsInvalidDependencyRange(string range) + public async Task RejectsInvalidDependencyRange(string range) { string archive = Archive("A.nupkg", Manifest("A", "1.0.0", $"")); ExpectedPackage package = Expected("A", "1.0.0", new Dictionary { ["B"] = "2.0.0" }); - Assert.Throws(typeof(InvalidOperationException), () => _verifier.Verify([archive], [package], false)); + await Assert.ThrowsAsync(() => _verifier.Verify([archive], [package], false)); } public void Dispose() diff --git a/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageBuilderTests.cs b/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageBuilderTests.cs index d74da79..ccfba58 100644 --- a/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageBuilderTests.cs +++ b/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageBuilderTests.cs @@ -56,7 +56,7 @@ public static class A { public static int Value => B.Value; } [Test] public async Task PacksIndependentVersionsAndPreservesDirectoryBuildImports() { - IReadOnlyList archives = await new PackageBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); + IReadOnlyList archives = await NewBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); using (Assert.Multiple()) { @@ -77,7 +77,7 @@ public async Task SelectsSdkFromSuppliedWorkingDirectory() """); InvalidOperationException? error = await Assert.ThrowsAsync(() => - new PackageBuilder().Build(Input([_projectA]) with { WorkingDirectory = nestedDirectory }, CancellationToken.None)); + NewBuilder().Build(Input([_projectA]) with { WorkingDirectory = nestedDirectory }, CancellationToken.None)); await Assert.That(error!.Message).Contains("99.0.100"); } @@ -91,7 +91,7 @@ public async Task IgnoresUnrelatedSolutionProjects() solution.Root!.Add(new XElement("Project", new XAttribute("Path", "Unrelated/Unrelated.csproj"))); solution.Save(_solutionPath); - IReadOnlyList archives = await new PackageBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); + IReadOnlyList archives = await NewBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); using (Assert.Multiple()) { @@ -117,7 +117,7 @@ public async Task BuildsSelectedProjectsInRequestedOrder(string first, string se targets.Save(Path.Combine(_directory, "Directory.Build.targets")); string[] projects = [Path.Combine(_directory, first, first + ".csproj"), Path.Combine(_directory, second, second + ".csproj")]; - await new PackageBuilder().Build(Input(projects), CancellationToken.None); + await NewBuilder().Build(Input(projects), CancellationToken.None); await Assert.That(File.ReadAllLines(orderPath).SequenceEqual([first, second])).IsTrue(); } @@ -125,7 +125,7 @@ public async Task BuildsSelectedProjectsInRequestedOrder(string first, string se [Test] public async Task PacksOnlySelectedProjectAgainstUnchangedDependencyAndIncludesSymbols() { - IReadOnlyList archives = await new PackageBuilder().Build(Input([_projectA], includeSymbols: true), CancellationToken.None); + IReadOnlyList archives = await NewBuilder().Build(Input([_projectA], includeSymbols: true), CancellationToken.None); using (Assert.Multiple()) { @@ -158,7 +158,7 @@ public static class A } """); - await new PackageBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); + await NewBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); XDocument manifest = ReadManifest("A.2.3.4.nupkg"); using (Assert.Multiple()) @@ -182,19 +182,18 @@ public async Task LateVersionOverrideIsRejectedBeforePacking() targets.Save(Path.Combine(_directory, "Directory.Build.targets")); InvalidOperationException? exception = await Assert.ThrowsAsync( - () => new PackageBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None)); + () => NewBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None)); using (Assert.Multiple()) { await Assert.That(exception!.Message).Contains("7.6.5"); - await Assert.That(Directory.GetFiles(_directory, "*.packagecascade-*.targets", SearchOption.AllDirectories)).IsEmpty(); await Assert.That(exception!.Message).Contains("2.3.4"); await Assert.That(Directory.GetFiles(_outputDirectory)).IsEmpty(); } } [Test] - public async Task PreservesExistingVersionHookAndCleansTemporaryImports() + public async Task PreservesExistingVersionHook() { string hookPath = Path.Combine(_directory, "consumer.targets"); File.WriteAllText(hookPath, """ @@ -212,14 +211,13 @@ public async Task PreservesExistingVersionHookAndCleansTemporaryImports() public static class A { public static int Value => B.Value; } """); - await new PackageBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); + await NewBuilder().Build(Input([_projectB, _projectA]), CancellationToken.None); using (Assert.Multiple()) { await AssertIdentity(ReadManifest("A.2.3.4.nupkg"), "A", "2.3.4"); await Assert.That(File.ReadAllText(Path.Combine(_directory, "A", "obj", "Release", "net11.0", "A.AssemblyInfo.cs"))) .Contains("AssemblyVersionAttribute(\"2.3.4.0\")"); - await Assert.That(Directory.GetFiles(_directory, "*.packagecascade-*.targets", SearchOption.AllDirectories)).IsEmpty(); await Assert.That(File.Exists(hookPath)).IsTrue(); } } @@ -239,13 +237,12 @@ public async Task PacksProjectUnderPathContainingApostropheAndSpecialCharacters( }, }; - await new PackageBuilder().Build(input, CancellationToken.None); + await NewBuilder().Build(input, CancellationToken.None); using (Assert.Multiple()) { await AssertIdentity(ReadManifest("A.2.3.4.nupkg"), "A", "2.3.4"); await AssertMinimumDependency(ReadManifest("A.2.3.4.nupkg").Descendants().Single(element => element.Name.LocalName == "dependency"), "B", "3.4.5"); - await Assert.That(Directory.GetFiles(_directory, "*.packagecascade-*.targets", SearchOption.AllDirectories)).IsEmpty(); } } @@ -254,6 +251,8 @@ public void Dispose() Directory.Delete(_directory, recursive: true); } + private static PackageBuilder NewBuilder() => new(new PackageVersionMapWriter()); + private PackageBuildInput Input(IReadOnlyList selectedProjects, bool includeSymbols = false) { return new(_directory, selectedProjects, new Dictionary diff --git a/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageVersionMapWriterTests.cs b/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageVersionMapWriterTests.cs new file mode 100644 index 0000000..0461917 --- /dev/null +++ b/src/PackageCascade.Tests.Unit/Internal/Executing/Packaging/PackageVersionMapWriterTests.cs @@ -0,0 +1,53 @@ +using System.Xml.Linq; +using PackageCascade.Internal.Executing.Packaging; + +namespace PackageCascade.Tests.Unit.Internal.Executing.Packaging; + +public sealed class PackageVersionMapWriterTests : IDisposable +{ + private readonly string _directory = Directory.CreateTempSubdirectory("packagecascade-map-writer-").FullName; + + [Test] + public async Task WritesConditionalVersions() + { + string path = Path.Combine(_directory, "obj", "A.csproj.packagecascade-run123.targets"); + PackageVersionMap map = new("run123", new Dictionary { [path] = "2.3.4" }); + + await new PackageVersionMapWriter().Write(map); + + XDocument document = XDocument.Load(path); + XElement group = document.Root!.Element("PropertyGroup")!; + using (Assert.Multiple()) + { + await Assert.That((string?)group.Attribute("Condition")).IsEqualTo("'$(PackageCascadeExecutionId)' == 'run123'"); + await Assert.That((string?)group.Element("Version")).IsEqualTo("2.3.4"); + await Assert.That((string?)group.Element("PackageVersion")).IsEqualTo("2.3.4"); + } + } + + [Test] + public async Task ExistingTargetIsNotOverwritten() + { + string createdPath = Path.Combine(_directory, "A.csproj.packagecascade-run123.targets"); + string existingPath = Path.Combine(_directory, "B.csproj.packagecascade-run123.targets"); + File.WriteAllText(existingPath, "consumer content"); + PackageVersionMap map = new("run123", new Dictionary + { + [createdPath] = "2.3.4", + [existingPath] = "3.4.5", + }); + + await Assert.ThrowsAsync(() => new PackageVersionMapWriter().Write(map)); + + using (Assert.Multiple()) + { + await Assert.That(File.Exists(createdPath)).IsTrue(); + await Assert.That(File.ReadAllText(existingPath)).IsEqualTo("consumer content"); + } + } + + public void Dispose() + { + Directory.Delete(_directory, recursive: true); + } +} diff --git a/src/PackageCascade.Tests.Unit/Internal/Executing/PlanExecutorTests.cs b/src/PackageCascade.Tests.Unit/Internal/Executing/PlanExecutorTests.cs index 8e5e75e..e61ef0c 100644 --- a/src/PackageCascade.Tests.Unit/Internal/Executing/PlanExecutorTests.cs +++ b/src/PackageCascade.Tests.Unit/Internal/Executing/PlanExecutorTests.cs @@ -1,7 +1,6 @@ using PackageCascade.Internal.Executing; using PackageCascade.Internal.Executing.Git; using PackageCascade.Internal.Executing.Packaging; -using PackageCascade.Internal.Git; using PackageCascade.Public.Executing; using PackageCascade.Public.Definition; @@ -14,8 +13,7 @@ public sealed class PlanExecutorTests : IDisposable private readonly StubPackageBuilder _builder; private readonly StubArchiveVerifier _verifier; private readonly StubTagWriter _tags; - private readonly StubHeadReader _commits; - private readonly ReleasePlan _plan = new([new("A", "src/A.csproj", "1.2.0", [], ["B"])], "head-sha", "Release", [new("B", "src/B.csproj", "2.0.0")], "."); + private readonly ReleasePlan _plan = new([new("A", "src/A.csproj", "1.2.0", [], ["B"])], "Release", [new("B", "src/B.csproj", "2.0.0")], "."); public PlanExecutorTests() { @@ -25,7 +23,6 @@ public PlanExecutorTests() _builder = new StubPackageBuilder(input => [WriteArchive(input.OutputDirectory)]); _verifier = new StubArchiveVerifier(); _tags = new StubTagWriter(); - _commits = new StubHeadReader(); } public void Dispose() @@ -42,10 +39,9 @@ public void Dispose() } [Test] - public async Task ForwardsPlanAndOptionsToCollaborators() + public async Task ForwardsPlanAndOptionsToCollaborators(CancellationToken cancellationToken) { - using CancellationTokenSource cancellation = new(); - await Execute(new(_outputDirectory, true), cancellation.Token); + await Execute(new(_outputDirectory, true), cancellationToken); using (Assert.Multiple()) { @@ -55,9 +51,10 @@ public async Task ForwardsPlanAndOptionsToCollaborators() await Assert.That(_builder.Input.VersionsByProjectPath[Path.Combine(_repositoryPath, "src/B.csproj")]).IsEqualTo("2.0.0"); await Assert.That(_builder.Input.Configuration).IsEqualTo("Release"); await Assert.That(_builder.Input.IncludeSymbols).IsTrue(); - await Assert.That(_builder.Token).IsEqualTo(cancellation.Token); + await Assert.That(_builder.Token).IsEqualTo(cancellationToken); await Assert.That(_verifier.Archives).IsEquivalentTo(new[] { Path.Combine(_builder.Input!.OutputDirectory, "A.1.2.0.nupkg") }); await Assert.That(_verifier.IncludeSymbols).IsTrue(); + await Assert.That(_verifier.Token).IsEqualTo(cancellationToken); await Assert.That(_verifier.Packages.Single().PackageId).IsEqualTo("A"); await Assert.That(_verifier.Packages.Single().Version).IsEqualTo("1.2.0"); await Assert.That(_verifier.Packages.Single().DirectDependencyVersions["B"]).IsEqualTo("2.0.0"); @@ -95,18 +92,18 @@ public async Task EmptyPlanValidatesInputsWithoutChangingOutput() await Assert.That(result.PackagePaths).IsEmpty(); await Assert.That(result.CreatedTags).IsEmpty(); await Assert.That(File.ReadAllText(existingFile)).IsEqualTo("keep this file"); - await Assert.That(_commits.ReadCount).IsEqualTo(1); await Assert.That(_builder.Input).IsNull(); } } [Test] - public async Task EmptyPlanStillRejectsInvalidInputs() + public async Task ResolvesReferencedProjectOutsideRepository() { - ReleasePlan invalid = _plan with { Packages = [], ReferencedPackages = [new("B", "../outside.csproj", "2.0.0")] }; + ReleasePlan plan = _plan with { ReferencedPackages = [new("B", "../outside.csproj", "2.0.0")] }; - await Assert.ThrowsAsync(() => - CreateExecutor().Execute(invalid, _repositoryPath, new(_outputDirectory))); + await CreateExecutor().Execute(plan, _repositoryPath, new(_outputDirectory)); + + await Assert.That(_builder.Input!.VersionsByProjectPath[Path.GetFullPath(Path.Combine(_repositoryPath, "../outside.csproj"))]).IsEqualTo("2.0.0"); } [Test] @@ -204,11 +201,11 @@ public async Task BuildFailureLeavesPartialStagingAndPreservesExistingFiles(bool } [Test] - public async Task ExistingArchiveNameCannotBeOverwritten() + public async Task ExistingArchiveNameIsOverwritten() { Directory.CreateDirectory(_outputDirectory); string existingArchive = Path.Combine(_outputDirectory, "A.1.2.0.snupkg"); - File.WriteAllText(existingArchive, "pre-existing symbol content"); + File.WriteAllText(existingArchive, "pre-existing symbol content longer than replacement"); _builder.BuildAction = input => { string package = WriteArchive(input.OutputDirectory); @@ -217,14 +214,15 @@ public async Task ExistingArchiveNameCannotBeOverwritten() return [package, symbols]; }; - await Assert.ThrowsAsync(() => Execute(new(_outputDirectory, true))); + ExecutionResult result = await Execute(new(_outputDirectory, true)); using (Assert.Multiple()) { - await Assert.That(File.ReadAllText(existingArchive)).IsEqualTo("pre-existing symbol content"); + await Assert.That(File.ReadAllText(existingArchive)).IsEqualTo("new symbol content"); await Assert.That(File.Exists(Path.Combine(_outputDirectory, "A.1.2.0.nupkg"))).IsTrue(); - await Assert.That(_tags.Plan).IsNull(); - await Assert.That(Directory.Exists(_builder.Input!.OutputDirectory)).IsTrue(); + await Assert.That(result.PackagePaths).Contains(existingArchive); + await Assert.That(_tags.Plan).IsEqualTo(_plan); + await Assert.That(Directory.Exists(_builder.Input!.OutputDirectory)).IsFalse(); } } @@ -248,24 +246,6 @@ public async Task ArchiveVerificationFailurePreservesExistingFilesAndPreventsTag } } - [Test] - public async Task ChangedHeadAfterBuildPreventsTagsAndLeavesVerifiedOutput() - { - _builder.BuildAction = input => - { - _commits.Head = "changed-head"; - return [WriteArchive(input.OutputDirectory)]; - }; - - await Assert.ThrowsAsync(() => Execute()); - - using (Assert.Multiple()) - { - await Assert.That(File.Exists(Path.Combine(_outputDirectory, "A.1.2.0.nupkg"))).IsTrue(); - await Assert.That(_tags.Plan).IsNull(); - } - } - [Test] public async Task UnrelatedRepositoryFilesCreatedDuringBuildDoNotBlockExecution() { @@ -324,7 +304,7 @@ public async Task TagCreationFailureLeavesVerifiedOutput() private Task Execute(ExecutionOptions? options = null, CancellationToken token = default) => CreateExecutor().Execute(_plan, _repositoryPath, options ?? new(_outputDirectory), token); - private PlanExecutor CreateExecutor() => new(_builder, _verifier, _tags, _commits); + private PlanExecutor CreateExecutor() => new(_builder, _verifier, _tags); private string WriteArchive(string? directory = null) { @@ -357,16 +337,21 @@ private sealed class StubArchiveVerifier : IPackageArchiveVerifier public IReadOnlyList Archives { get; private set; } = []; public IReadOnlyList Packages { get; private set; } = []; public bool IncludeSymbols { get; private set; } + public CancellationToken Token { get; private set; } public Exception? Error { get; set; } - public void Verify(IReadOnlyList archivePaths, IReadOnlyList packages, bool includeSymbols) + public Task Verify(IReadOnlyList archivePaths, IReadOnlyList packages, bool includeSymbols, + CancellationToken cancellationToken = default) { Archives = archivePaths; Packages = packages; IncludeSymbols = includeSymbols; + Token = cancellationToken; if (Error is not null) { throw Error; } + + return Task.CompletedTask; } } @@ -389,15 +374,4 @@ public IReadOnlyList Create(string repositoryPath, ReleasePlan plan) return ["A-v1.2.0"]; } } - - private sealed class StubHeadReader : IRepositoryHeadReader - { - public string Head { get; set; } = "head-sha"; - public int ReadCount { get; private set; } - public string ReadHeadSha(string repositoryPath) - { - ReadCount++; - return Head; - } - } } diff --git a/src/PackageCascade.Tests.Unit/Internal/Git/RepositoryHeadReaderTests.cs b/src/PackageCascade.Tests.Unit/Internal/Git/RepositoryHeadReaderTests.cs deleted file mode 100644 index 2961c98..0000000 --- a/src/PackageCascade.Tests.Unit/Internal/Git/RepositoryHeadReaderTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using LibGit2Sharp; -using PackageCascade.Internal.Git; - -namespace PackageCascade.Tests.Unit.Internal.Git; - -public sealed class RepositoryHeadReaderTests : IDisposable -{ - private readonly string _directory = Directory.CreateTempSubdirectory("packagecascade-head-").FullName; - private readonly Repository _repository; - - public RepositoryHeadReaderTests() - { - Repository.Init(_directory); - _repository = new(_directory); - } - - [Test] - public async Task ReadsHeadCommitSha() - { - File.WriteAllText(Path.Combine(_directory, "file.txt"), "content"); - Commands.Stage(_repository, "file.txt"); - Signature author = new("PackageCascade Tests", "tests@example.invalid", DateTimeOffset.UtcNow); - Commit commit = _repository.Commit("chore: initial", author, author); - - await Assert.That(new RepositoryHeadReader().ReadHeadSha(_directory)).IsEqualTo(commit.Sha); - } - - public void Dispose() - { - _repository.Dispose(); - Directory.Delete(_directory, recursive: true); - } -} diff --git a/src/PackageCascade.Tests.Unit/Internal/Planning/PlanGeneratorTests.cs b/src/PackageCascade.Tests.Unit/Internal/Planning/PlanGeneratorTests.cs index 1553c15..6c0df0e 100644 --- a/src/PackageCascade.Tests.Unit/Internal/Planning/PlanGeneratorTests.cs +++ b/src/PackageCascade.Tests.Unit/Internal/Planning/PlanGeneratorTests.cs @@ -1,5 +1,4 @@ using PackageCascade.Internal.Planning.Git; -using PackageCascade.Internal.Git; using PackageCascade.Internal.Planning.Projects; using PackageCascade.Internal.Planning.SemVer; using PackageCascade.Public.Planning; @@ -275,7 +274,7 @@ public async Task SameInputsProduceIdenticalSerialisedPlans() [Test] [Arguments("Debug")] [Arguments("Staging")] - public async Task IncludesHeadShaAndRequestedConfiguration(string configuration) + public async Task IncludesRequestedConfiguration(string configuration) { StubCommitReader commits = new([]); StubDiscoverer discoverer = new(Graph()); @@ -285,7 +284,6 @@ public async Task IncludesHeadShaAndRequestedConfiguration(string configuration) using (Assert.Multiple()) { - await Assert.That(plan.TargetCommitSha).IsEqualTo("head-sha"); await Assert.That(plan.Configuration).IsEqualTo(configuration); await Assert.That(discoverer.Configuration).IsEqualTo(configuration); } @@ -316,7 +314,7 @@ private static PlanGenerator CreateGenerator(IReadOnlyList proj private static PlanGenerator CreateGenerator(StubDiscoverer discoverer, IGitCommitReader commits, IReadOnlyDictionary tags) - => new(discoverer, commits, new StubHeadReader(), new StubTagReader(tags), new StubGraphBuilder(), + => new(discoverer, commits, new StubTagReader(tags), new StubGraphBuilder(), new StubDirectoryBumpCalculator(), new StubPackageBumpCalculator()); private static async Task AssertVersions(ReleasePlan plan, params (string PackageId, string Version)[] expected) @@ -396,11 +394,6 @@ public IReadOnlyList ReadSince(string repositoryPath, string b => _commitsByBaseline is null ? _commits : _commitsByBaseline[baselineCommitHash]; } - private sealed class StubHeadReader : IRepositoryHeadReader - { - public string ReadHeadSha(string repositoryPath) => "head-sha"; - } - private sealed class StubGraphBuilder : IPackageDependencyGraphBuilder { public IReadOnlyDictionary> Build(IReadOnlyList projects) diff --git a/src/PackageCascade/Internal/Executing/Git/PackageTagWriter.cs b/src/PackageCascade/Internal/Executing/Git/PackageTagWriter.cs index 6f96b06..2f5cb59 100644 --- a/src/PackageCascade/Internal/Executing/Git/PackageTagWriter.cs +++ b/src/PackageCascade/Internal/Executing/Git/PackageTagWriter.cs @@ -8,8 +8,7 @@ internal sealed class PackageTagWriter : IPackageTagWriter public IReadOnlyList Create(string repositoryPath, ReleasePlan plan) { using Repository repository = new(repositoryPath); - Commit target = repository.Lookup(plan.TargetCommitSha) - ?? throw new InvalidOperationException($"Target commit '{plan.TargetCommitSha}' was not found."); + Commit target = repository.Head.Tip ?? throw new InvalidOperationException("The repository has no HEAD commit."); List created = []; foreach (PlannedRelease release in plan.Packages) diff --git a/src/PackageCascade/Internal/Executing/OutputDirectoryScope.cs b/src/PackageCascade/Internal/Executing/OutputDirectoryScope.cs index c8e6e38..9f7e99c 100644 --- a/src/PackageCascade/Internal/Executing/OutputDirectoryScope.cs +++ b/src/PackageCascade/Internal/Executing/OutputDirectoryScope.cs @@ -21,7 +21,7 @@ public string CreateStagingDirectory() return _stagingPath ??= Directory.CreateTempSubdirectory("packagecascade-apply-").FullName; } - public IReadOnlyList CopyVerifiedArchives(IReadOnlyList archivePaths) + public async Task> CopyVerifiedArchives(IReadOnlyList archivePaths, CancellationToken cancellationToken) { if (_stagingPath is null) { @@ -34,6 +34,7 @@ public IReadOnlyList CopyVerifiedArchives(IReadOnlyList archiveP ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; foreach (string archivePath in archivePaths) { + cancellationToken.ThrowIfCancellationRequested(); string source = Path.GetFullPath(archivePath); if (!string.Equals(Path.GetDirectoryName(source), _stagingPath, pathComparison)) { @@ -41,10 +42,21 @@ public IReadOnlyList CopyVerifiedArchives(IReadOnlyList archiveP } string destination = Path.Combine(_path, Path.GetFileName(source)); - using FileStream output = new(destination, FileMode.CreateNew, FileAccess.Write); + await using FileStream output = new(destination, new FileStreamOptions + { + Mode = FileMode.Create, + Access = FileAccess.Write, + Options = FileOptions.Asynchronous, + }); copied.Add(destination); - using FileStream input = File.OpenRead(source); - input.CopyTo(output); + await using FileStream input = new(source, new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous | FileOptions.SequentialScan, + }); + await input.CopyToAsync(output, cancellationToken); } return copied; diff --git a/src/PackageCascade/Internal/Executing/Packaging/IPackageArchiveVerifier.cs b/src/PackageCascade/Internal/Executing/Packaging/IPackageArchiveVerifier.cs index e64f922..ccdecfa 100644 --- a/src/PackageCascade/Internal/Executing/Packaging/IPackageArchiveVerifier.cs +++ b/src/PackageCascade/Internal/Executing/Packaging/IPackageArchiveVerifier.cs @@ -2,5 +2,6 @@ namespace PackageCascade.Internal.Executing.Packaging; internal interface IPackageArchiveVerifier { - public void Verify(IReadOnlyList archivePaths, IReadOnlyList packages, bool includeSymbols); + public Task Verify(IReadOnlyList archivePaths, IReadOnlyList packages, + bool includeSymbols, CancellationToken cancellationToken = default); } diff --git a/src/PackageCascade/Internal/Executing/Packaging/IPackageVersionMapWriter.cs b/src/PackageCascade/Internal/Executing/Packaging/IPackageVersionMapWriter.cs new file mode 100644 index 0000000..926c1d9 --- /dev/null +++ b/src/PackageCascade/Internal/Executing/Packaging/IPackageVersionMapWriter.cs @@ -0,0 +1,6 @@ +namespace PackageCascade.Internal.Executing.Packaging; + +internal interface IPackageVersionMapWriter +{ + public Task Write(PackageVersionMap versionMap, CancellationToken cancellationToken = default); +} diff --git a/src/PackageCascade/Internal/Executing/Packaging/PackageArchiveVerifier.cs b/src/PackageCascade/Internal/Executing/Packaging/PackageArchiveVerifier.cs index 33b9fa4..ba5822f 100644 --- a/src/PackageCascade/Internal/Executing/Packaging/PackageArchiveVerifier.cs +++ b/src/PackageCascade/Internal/Executing/Packaging/PackageArchiveVerifier.cs @@ -7,7 +7,24 @@ namespace PackageCascade.Internal.Executing.Packaging; internal sealed class PackageArchiveVerifier : IPackageArchiveVerifier { - public void Verify(IReadOnlyList archivePaths, IReadOnlyList packages, bool includeSymbols) + public async Task Verify(IReadOnlyList archivePaths, IReadOnlyList packages, + bool includeSymbols, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + Dictionary expected = IndexExpectedPackages(packages); + HashSet packageIds = new(StringComparer.OrdinalIgnoreCase); + HashSet symbolIds = new(StringComparer.OrdinalIgnoreCase); + foreach (string path in archivePaths) + { + cancellationToken.ThrowIfCancellationRequested(); + await VerifyArchive(path, expected, packageIds, symbolIds, cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + VerifyCompleteness(packages, includeSymbols, packageIds, symbolIds); + } + + private static Dictionary IndexExpectedPackages(IReadOnlyList packages) { Dictionary expected = new(StringComparer.OrdinalIgnoreCase); foreach (ExpectedPackage package in packages) @@ -18,39 +35,50 @@ public void Verify(IReadOnlyList archivePaths, IReadOnlyList packageIds = new(StringComparer.OrdinalIgnoreCase); - HashSet symbolIds = new(StringComparer.OrdinalIgnoreCase); - foreach (string path in archivePaths) + return expected; + } + + private static async Task VerifyArchive(string path, Dictionary expected, + HashSet packageIds, HashSet symbolIds, CancellationToken cancellationToken) + { + bool isSymbol = path.EndsWith(".snupkg", StringComparison.OrdinalIgnoreCase); + if (!isSymbol && !path.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase)) { - bool isSymbol = path.EndsWith(".snupkg", StringComparison.OrdinalIgnoreCase); - if (!isSymbol && !path.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"Unexpected archive type: '{path}'."); - } + throw new InvalidOperationException($"Unexpected archive type: '{path}'."); + } - XElement metadata = ReadMetadata(path); - string id = RequiredElement(metadata, "id", path).Value; - string versionText = RequiredElement(metadata, "version", path).Value; - if (!expected.TryGetValue(id, out ExpectedPackage? package) - || !NuGetVersion.TryParse(versionText, out NuGetVersion? version) - || !NuGetVersion.TryParse(package.Version, out NuGetVersion? expectedVersion) - || version != expectedVersion) - { - throw new InvalidOperationException($"Unexpected package identity '{id}' version '{versionText}' in '{path}'."); - } + XElement metadata = await ReadMetadata(path, cancellationToken); + ExpectedPackage package = VerifyIdentity(metadata, expected, path); + HashSet seen = isSymbol ? symbolIds : packageIds; + if (!seen.Add(package.PackageId)) + { + throw new InvalidOperationException($"Duplicate {(isSymbol ? "symbol" : "package")} archive for '{package.PackageId}'."); + } - HashSet seen = isSymbol ? symbolIds : packageIds; - if (!seen.Add(id)) - { - throw new InvalidOperationException($"Duplicate {(isSymbol ? "symbol" : "package")} archive for '{id}'."); - } + if (!isSymbol) + { + VerifyDependencies(metadata, package, path); + } + } - if (!isSymbol) - { - VerifyDependencies(metadata, package, path); - } + private static ExpectedPackage VerifyIdentity(XElement metadata, Dictionary expected, string path) + { + string id = RequiredElement(metadata, "id", path).Value; + string versionText = RequiredElement(metadata, "version", path).Value; + if (!expected.TryGetValue(id, out ExpectedPackage? package) + || !NuGetVersion.TryParse(versionText, out NuGetVersion? version) + || !NuGetVersion.TryParse(package.Version, out NuGetVersion? expectedVersion) + || version != expectedVersion) + { + throw new InvalidOperationException($"Unexpected package identity '{id}' version '{versionText}' in '{path}'."); } + return package; + } + + private static void VerifyCompleteness(IReadOnlyList packages, bool includeSymbols, + HashSet packageIds, HashSet symbolIds) + { foreach (ExpectedPackage package in packages) { if (!packageIds.Contains(package.PackageId)) @@ -65,35 +93,21 @@ public void Verify(IReadOnlyList archivePaths, IReadOnlyList ReadMetadata(string path, CancellationToken cancellationToken) { try { - using ZipArchive archive = ZipFile.OpenRead(path); - ZipArchiveEntry[] manifests = archive.Entries - .Where(entry => !entry.FullName.Contains('/') && !entry.FullName.Contains('\\') - && entry.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)) - .ToArray(); - if (manifests.Length != 1) - { - throw new InvalidOperationException($"Archive '{path}' must contain exactly one root nuspec."); - } - - using Stream stream = manifests[0].Open(); - using XmlReader reader = XmlReader.Create(stream, new XmlReaderSettings + await using FileStream file = new(path, new FileStreamOptions { - DtdProcessing = DtdProcessing.Prohibit, - XmlResolver = null, + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous | FileOptions.SequentialScan, }); - XElement root = XDocument.Load(reader).Root - ?? throw new InvalidOperationException($"Archive '{path}' has an empty nuspec."); - if (root.Name.LocalName != "package") - { - throw new InvalidOperationException($"Archive '{path}' has an invalid nuspec root."); - } - - return root.Element(root.Name.Namespace + "metadata") - ?? throw new InvalidOperationException($"Archive '{path}' has no nuspec metadata."); + await using ZipArchive archive = await ZipArchive.CreateAsync(file, ZipArchiveMode.Read, + leaveOpen: true, entryNameEncoding: null, cancellationToken); + ZipArchiveEntry manifest = FindManifest(archive, path); + return await LoadMetadata(manifest, path, cancellationToken); } catch (Exception exception) when (exception is InvalidDataException or XmlException) { @@ -101,6 +115,40 @@ private static XElement ReadMetadata(string path) } } + private static ZipArchiveEntry FindManifest(ZipArchive archive, string path) + { + ZipArchiveEntry[] manifests = archive.Entries + .Where(entry => !entry.FullName.Contains('/') && !entry.FullName.Contains('\\') + && entry.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + if (manifests.Length != 1) + { + throw new InvalidOperationException($"Archive '{path}' must contain exactly one root nuspec."); + } + + return manifests[0]; + } + + private static async Task LoadMetadata(ZipArchiveEntry manifest, string path, CancellationToken cancellationToken) + { + await using Stream stream = await manifest.OpenAsync(cancellationToken); + using XmlReader reader = XmlReader.Create(stream, new XmlReaderSettings + { + Async = true, + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + }); + XElement root = (await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken)).Root + ?? throw new InvalidOperationException($"Archive '{path}' has an empty nuspec."); + if (root.Name.LocalName != "package") + { + throw new InvalidOperationException($"Archive '{path}' has an invalid nuspec root."); + } + + return root.Element(root.Name.Namespace + "metadata") + ?? throw new InvalidOperationException($"Archive '{path}' has no nuspec metadata."); + } + private static XElement RequiredElement(XElement parent, string name, string path) { return parent.Element(parent.Name.Namespace + name) @@ -108,6 +156,31 @@ private static XElement RequiredElement(XElement parent, string name, string pat } private static void VerifyDependencies(XElement metadata, ExpectedPackage package, string path) + { + Dictionary expectedDependencies = ParseExpectedDependencies(package); + HashSet found = new(StringComparer.OrdinalIgnoreCase); + foreach (XElement dependency in GetDependencyEntries(metadata)) + { + string? id = (string?)dependency.Attribute("id"); + if (id is null || !expectedDependencies.TryGetValue(id, out NuGetVersion? expectedVersion)) + { + continue; + } + + VerifyDependencyRange(dependency, package.PackageId, id, expectedVersion, path); + found.Add(id); + } + + foreach (string id in expectedDependencies.Keys) + { + if (!found.Contains(id)) + { + throw new InvalidOperationException($"Package '{package.PackageId}' is missing dependency '{id}' in '{path}'."); + } + } + } + + private static Dictionary ParseExpectedDependencies(ExpectedPackage package) { Dictionary expectedDependencies = new(StringComparer.OrdinalIgnoreCase); foreach ((string id, string version) in package.DirectDependencyVersions) @@ -120,38 +193,30 @@ private static void VerifyDependencies(XElement metadata, ExpectedPackage packag expectedDependencies.Add(id, parsed); } - HashSet found = new(StringComparer.OrdinalIgnoreCase); + return expectedDependencies; + } + + private static IEnumerable GetDependencyEntries(XElement metadata) + { XElement? dependencies = metadata.Element(metadata.Name.Namespace + "dependencies"); - if (dependencies is not null) + if (dependencies is null) { - IEnumerable entries = dependencies.Elements(dependencies.Name.Namespace + "dependency") - .Concat(dependencies.Elements(dependencies.Name.Namespace + "group") - .SelectMany(group => group.Elements(group.Name.Namespace + "dependency"))); - foreach (XElement dependency in entries) - { - string? id = (string?)dependency.Attribute("id"); - if (id is null || !expectedDependencies.TryGetValue(id, out NuGetVersion? expectedVersion)) - { - continue; - } - - string? version = (string?)dependency.Attribute("version"); - if (version is null || !VersionRange.TryParse(version, out VersionRange? range) - || range.IsFloating || !range.IsMinInclusive || range.MinVersion != expectedVersion) - { - throw new InvalidOperationException($"Package '{package.PackageId}' has an invalid dependency range '{version}' for '{id}' in '{path}'; expected an inclusive minimum of '{expectedVersion}'."); - } - - found.Add(id); - } + return []; } - foreach (string id in expectedDependencies.Keys) + return dependencies.Elements(dependencies.Name.Namespace + "dependency") + .Concat(dependencies.Elements(dependencies.Name.Namespace + "group") + .SelectMany(group => group.Elements(group.Name.Namespace + "dependency"))); + } + + private static void VerifyDependencyRange(XElement dependency, string packageId, string id, + NuGetVersion expectedVersion, string path) + { + string? version = (string?)dependency.Attribute("version"); + if (version is null || !VersionRange.TryParse(version, out VersionRange? range) + || range.IsFloating || !range.IsMinInclusive || range.MinVersion != expectedVersion) { - if (!found.Contains(id)) - { - throw new InvalidOperationException($"Package '{package.PackageId}' is missing dependency '{id}' in '{path}'."); - } + throw new InvalidOperationException($"Package '{packageId}' has an invalid dependency range '{version}' for '{id}' in '{path}'; expected an inclusive minimum of '{expectedVersion}'."); } } } diff --git a/src/PackageCascade/Internal/Executing/Packaging/PackageBuilder.cs b/src/PackageCascade/Internal/Executing/Packaging/PackageBuilder.cs index 7c22701..1b12b41 100644 --- a/src/PackageCascade/Internal/Executing/Packaging/PackageBuilder.cs +++ b/src/PackageCascade/Internal/Executing/Packaging/PackageBuilder.cs @@ -5,17 +5,25 @@ namespace PackageCascade.Internal.Executing.Packaging; internal sealed class PackageBuilder : IPackageBuilder { + private readonly IPackageVersionMapWriter _versionMapWriter; + + public PackageBuilder(IPackageVersionMapWriter versionMapWriter) + { + _versionMapWriter = versionMapWriter; + } + public async Task> Build(PackageBuildInput input, CancellationToken cancellationToken) { string workingDirectory = Path.GetFullPath(input.WorkingDirectory); string outputDirectory = Path.GetFullPath(input.OutputDirectory); Directory.CreateDirectory(outputDirectory); - using ProjectVersionMap versionMap = new(); - string versionMapProperty = $"-p:PackageCascadeExecutionId={versionMap.ExecutionId}"; + string executionId = Guid.NewGuid().ToString("N"); + string versionMapProperty = $"-p:PackageCascadeExecutionId={executionId}"; string configurationProperty = $"-p:Configuration={input.Configuration}"; - await WriteVersionMap(input.VersionsByProjectPath, versionMap, workingDirectory, + PackageVersionMap versionMap = await CreateVersionMap(input.VersionsByProjectPath, executionId, workingDirectory, versionMapProperty, configurationProperty, cancellationToken); + await _versionMapWriter.Write(versionMap, cancellationToken); foreach (string projectPath in input.ProjectPaths) { @@ -44,14 +52,16 @@ await RunDotnet(workingDirectory, cancellationToken, .ToArray(); } - private static async Task WriteVersionMap( + private static async Task CreateVersionMap( IReadOnlyDictionary versionsByProjectPath, - ProjectVersionMap versionMap, + string executionId, string workingDirectory, string versionMapProperty, string configurationProperty, CancellationToken cancellationToken) { + Dictionary versionsByTargetPath = new( + OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); foreach ((string projectPath, string version) in versionsByProjectPath) { string fullPath = Path.GetFullPath(projectPath); @@ -62,9 +72,14 @@ private static async Task WriteVersionMap( { string extensionsPath = (await GetProperty(fullPath, "MSBuildProjectExtensionsPath", workingDirectory, versionMapProperty, configurationProperty, targetFramework, cancellationToken)).Trim(); - versionMap.Write(fullPath, Path.GetFullPath(extensionsPath, Path.GetDirectoryName(fullPath)!), version); + string targetPath = Path.Combine( + Path.GetFullPath(extensionsPath, Path.GetDirectoryName(fullPath)!), + $"{Path.GetFileName(fullPath)}.packagecascade-{executionId}.targets"); + versionsByTargetPath.TryAdd(targetPath, version); } } + + return new PackageVersionMap(executionId, versionsByTargetPath); } private static async Task EnsureEvaluatedVersions( diff --git a/src/PackageCascade/Internal/Executing/Packaging/PackageVersionMap.cs b/src/PackageCascade/Internal/Executing/Packaging/PackageVersionMap.cs new file mode 100644 index 0000000..07d5928 --- /dev/null +++ b/src/PackageCascade/Internal/Executing/Packaging/PackageVersionMap.cs @@ -0,0 +1,5 @@ +namespace PackageCascade.Internal.Executing.Packaging; + +internal sealed record PackageVersionMap( + string ExecutionId, + IReadOnlyDictionary VersionsByTargetPath); diff --git a/src/PackageCascade/Internal/Executing/Packaging/PackageVersionMapWriter.cs b/src/PackageCascade/Internal/Executing/Packaging/PackageVersionMapWriter.cs new file mode 100644 index 0000000..c8dc023 --- /dev/null +++ b/src/PackageCascade/Internal/Executing/Packaging/PackageVersionMapWriter.cs @@ -0,0 +1,29 @@ +using System.Xml.Linq; + +namespace PackageCascade.Internal.Executing.Packaging; + +internal sealed class PackageVersionMapWriter : IPackageVersionMapWriter +{ + public async Task Write(PackageVersionMap versionMap, CancellationToken cancellationToken = default) + { + foreach ((string path, string version) in versionMap.VersionsByTargetPath) + { + cancellationToken.ThrowIfCancellationRequested(); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + await using FileStream stream = new(path, new FileStreamOptions + { + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Options = FileOptions.Asynchronous, + }); + + // Project-extension imports preserve consumer hooks; the run ID isolates concurrent builds. + XElement project = new("Project", + new XElement("PropertyGroup", + new XAttribute("Condition", $"'$(PackageCascadeExecutionId)' == '{versionMap.ExecutionId}'"), + new XElement("Version", version), + new XElement("PackageVersion", version))); + await new XDocument(project).SaveAsync(stream, SaveOptions.None, cancellationToken); + } + } +} diff --git a/src/PackageCascade/Internal/Executing/Packaging/ProjectVersionMap.cs b/src/PackageCascade/Internal/Executing/Packaging/ProjectVersionMap.cs deleted file mode 100644 index 50d7226..0000000 --- a/src/PackageCascade/Internal/Executing/Packaging/ProjectVersionMap.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Xml.Linq; - -namespace PackageCascade.Internal.Executing.Packaging; - -internal sealed class ProjectVersionMap : IDisposable -{ - private readonly HashSet _paths = new(OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); - - public string ExecutionId { get; } = Guid.NewGuid().ToString("N"); - - public void Write(string projectPath, string extensionsPath, string version) - { - string path = Path.Combine(extensionsPath, $"{Path.GetFileName(projectPath)}.packagecascade-{ExecutionId}.targets"); - if (_paths.Contains(path)) - { - return; - } - - Directory.CreateDirectory(extensionsPath); - using FileStream stream = new(path, FileMode.CreateNew, FileAccess.Write); - _paths.Add(path); - - // Project-extension imports preserve consumer hooks; the run ID isolates concurrent builds. - XElement project = new("Project", - new XElement("PropertyGroup", - new XAttribute("Condition", $"'$(PackageCascadeExecutionId)' == '{ExecutionId}'"), - new XElement("Version", version), - new XElement("PackageVersion", version))); - new XDocument(project).Save(stream); - } - - public void Dispose() - { - List errors = []; - foreach (string path in _paths) - { - try - { - File.Delete(path); - } - catch (Exception exception) - { - errors.Add(exception); - } - } - - if (errors.Count > 0) - { - throw new AggregateException("Some temporary project version files could not be removed.", errors); - } - } -} diff --git a/src/PackageCascade/Internal/Executing/PlanExecutor.cs b/src/PackageCascade/Internal/Executing/PlanExecutor.cs index c137d0c..8d81131 100644 --- a/src/PackageCascade/Internal/Executing/PlanExecutor.cs +++ b/src/PackageCascade/Internal/Executing/PlanExecutor.cs @@ -1,6 +1,5 @@ using PackageCascade.Internal.Executing.Git; using PackageCascade.Internal.Executing.Packaging; -using PackageCascade.Internal.Git; using PackageCascade.Public.Executing; using PackageCascade.Public.Definition; using NuGet.Versioning; @@ -12,18 +11,15 @@ internal sealed class PlanExecutor : IPackageCascadeExecutor private readonly IPackageBuilder _packageBuilder; private readonly IPackageArchiveVerifier _archiveVerifier; private readonly IPackageTagWriter _tagWriter; - private readonly IRepositoryHeadReader _headReader; public PlanExecutor( IPackageBuilder packageBuilder, IPackageArchiveVerifier archiveVerifier, - IPackageTagWriter tagWriter, - IRepositoryHeadReader headReader) + IPackageTagWriter tagWriter) { _packageBuilder = packageBuilder; _archiveVerifier = archiveVerifier; _tagWriter = tagWriter; - _headReader = headReader; } public async Task Execute( @@ -35,11 +31,6 @@ public async Task Execute( cancellationToken.ThrowIfCancellationRequested(); string repositoryRoot = Path.GetFullPath(repositoryPath); OutputDirectoryScope output = new(options.OutputDirectory); - if (!_headReader.ReadHeadSha(repositoryRoot).Equals(plan.TargetCommitSha, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("The release plan targets a different HEAD commit."); - } - PackageBuildInput buildInput = CreateBuildInput(plan, repositoryRoot, output.OutputPath, options.IncludeSymbols); ExpectedPackage[] expected = CreateExpectedPackages(plan); if (plan.Packages.Count == 0) @@ -50,11 +41,10 @@ public async Task Execute( string stagingPath = output.CreateStagingDirectory(); IReadOnlyList archives = await _packageBuilder.Build( buildInput with { OutputDirectory = stagingPath }, cancellationToken); - _archiveVerifier.Verify(archives, expected, options.IncludeSymbols); - IReadOnlyList packagePaths = output.CopyVerifiedArchives(archives); + await _archiveVerifier.Verify(archives, expected, options.IncludeSymbols, cancellationToken); + IReadOnlyList packagePaths = await output.CopyVerifiedArchives(archives, cancellationToken); output.FinishStaging(); - EnsureHeadUnchanged(repositoryRoot, plan); cancellationToken.ThrowIfCancellationRequested(); IReadOnlyList createdTags = _tagWriter.Create(repositoryRoot, plan); @@ -62,14 +52,6 @@ public async Task Execute( return new(packagePaths, createdTags); } - private void EnsureHeadUnchanged(string repositoryRoot, ReleasePlan plan) - { - if (!_headReader.ReadHeadSha(repositoryRoot).Equals(plan.TargetCommitSha, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException("Repository HEAD changed while the release plan was being applied."); - } - } - private static PackageBuildInput CreateBuildInput(ReleasePlan plan, string repositoryRoot, string outputDirectory, bool includeSymbols) { @@ -119,19 +101,7 @@ private static ExpectedPackage[] CreateExpectedPackages(ReleasePlan plan) private static string ResolveProjectPath(string repositoryRoot, string projectPath) { - if (Path.IsPathRooted(projectPath)) - { - throw new InvalidOperationException($"Planned project path '{projectPath}' must be relative to the repository."); - } - - string fullPath = Path.GetFullPath(Path.Combine(repositoryRoot, projectPath)); - string relativePath = Path.GetRelativePath(repositoryRoot, fullPath); - if (relativePath == ".." || relativePath.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal)) - { - throw new InvalidOperationException($"Planned project path '{projectPath}' escapes the repository."); - } - - return fullPath; + return Path.GetFullPath(projectPath, repositoryRoot); } private static void ValidateVersion(string packageId, string version) diff --git a/src/PackageCascade/Internal/Git/IRepositoryHeadReader.cs b/src/PackageCascade/Internal/Git/IRepositoryHeadReader.cs deleted file mode 100644 index f992be0..0000000 --- a/src/PackageCascade/Internal/Git/IRepositoryHeadReader.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PackageCascade.Internal.Git; - -internal interface IRepositoryHeadReader -{ - public string ReadHeadSha(string repositoryPath); -} diff --git a/src/PackageCascade/Internal/Git/RepositoryHeadReader.cs b/src/PackageCascade/Internal/Git/RepositoryHeadReader.cs deleted file mode 100644 index f397951..0000000 --- a/src/PackageCascade/Internal/Git/RepositoryHeadReader.cs +++ /dev/null @@ -1,12 +0,0 @@ -using LibGit2Sharp; - -namespace PackageCascade.Internal.Git; - -internal sealed class RepositoryHeadReader : IRepositoryHeadReader -{ - public string ReadHeadSha(string repositoryPath) - { - using Repository repository = new(repositoryPath); - return repository.Head.Tip?.Sha ?? throw new InvalidOperationException("The repository has no HEAD commit."); - } -} diff --git a/src/PackageCascade/Internal/Planning/PlanGenerator.cs b/src/PackageCascade/Internal/Planning/PlanGenerator.cs index 19840cc..3fe92f9 100644 --- a/src/PackageCascade/Internal/Planning/PlanGenerator.cs +++ b/src/PackageCascade/Internal/Planning/PlanGenerator.cs @@ -1,5 +1,4 @@ using PackageCascade.Internal.Planning.Git; -using PackageCascade.Internal.Git; using PackageCascade.Internal.Planning.Projects; using PackageCascade.Internal.Planning.SemVer; using NuGet.Versioning; @@ -12,7 +11,6 @@ internal sealed class PlanGenerator : IPackageCascadePlanner { private readonly ISolutionPackageDiscoverer _packageDiscoverer; private readonly IGitCommitReader _commitReader; - private readonly IRepositoryHeadReader _headReader; private readonly IPackageTagReader _tagReader; private readonly IPackageDependencyGraphBuilder _graphBuilder; private readonly IDirectoryBumpCalculator _directoryCalculator; @@ -21,7 +19,6 @@ internal sealed class PlanGenerator : IPackageCascadePlanner public PlanGenerator( ISolutionPackageDiscoverer packageDiscoverer, IGitCommitReader commitReader, - IRepositoryHeadReader headReader, IPackageTagReader tagReader, IPackageDependencyGraphBuilder graphBuilder, IDirectoryBumpCalculator directoryCalculator, @@ -29,7 +26,6 @@ public PlanGenerator( { _packageDiscoverer = packageDiscoverer; _commitReader = commitReader; - _headReader = headReader; _tagReader = tagReader; _graphBuilder = graphBuilder; _directoryCalculator = directoryCalculator; @@ -59,7 +55,7 @@ public ReleasePlan Generate( IReadOnlyList referencedPackages = GetReferencedPackages(repositoryPath, selected, graph, tags); string workingDirectory = Path.GetRelativePath(repositoryPath, Path.GetDirectoryName(Path.GetFullPath(solutionPath))!) .Replace('\\', '/'); - return new(packages, _headReader.ReadHeadSha(repositoryPath), configuration, referencedPackages, workingDirectory); + return new(packages, configuration, referencedPackages, workingDirectory); } private static IReadOnlyList GetReferencedPackages( diff --git a/src/PackageCascade/Public/Definition/PlannedRelease.cs b/src/PackageCascade/Public/Definition/PlannedRelease.cs index 8691101..2cca4ec 100644 --- a/src/PackageCascade/Public/Definition/PlannedRelease.cs +++ b/src/PackageCascade/Public/Definition/PlannedRelease.cs @@ -2,7 +2,7 @@ namespace PackageCascade.Public.Definition; /// The proposed release of one packable project. /// The package's evaluated ID. -/// The project file path relative to the repository root, using forward slashes. +/// The project file path; relative paths are resolved from the repository root. /// The proposed package version as a normalised string. /// The commit, dependency, or initial-release reasons for this release. /// The package IDs of directly referenced packable projects, including unchanged packages. diff --git a/src/PackageCascade/Public/Definition/ReferencedPackage.cs b/src/PackageCascade/Public/Definition/ReferencedPackage.cs index 29c94d9..02a9ce4 100644 --- a/src/PackageCascade/Public/Definition/ReferencedPackage.cs +++ b/src/PackageCascade/Public/Definition/ReferencedPackage.cs @@ -2,6 +2,6 @@ namespace PackageCascade.Public.Definition; /// An unchanged packable project required to build a selected release. /// The package's evaluated ID. -/// The project file path relative to the repository root, using forward slashes. +/// The project file path; relative paths are resolved from the repository root. /// The package's latest reachable released version as a normalised string. public sealed record ReferencedPackage(string PackageId, string ProjectPath, string Version); diff --git a/src/PackageCascade/Public/Definition/ReleasePlan.cs b/src/PackageCascade/Public/Definition/ReleasePlan.cs index 589271a..60a514c 100644 --- a/src/PackageCascade/Public/Definition/ReleasePlan.cs +++ b/src/PackageCascade/Public/Definition/ReleasePlan.cs @@ -1,14 +1,12 @@ namespace PackageCascade.Public.Definition; -/// Packages to release, calculated from a specific repository commit and build configuration. +/// Packages to release, calculated for a build configuration. /// The selected packages in dependency-first order. -/// The full SHA of the repository HEAD used to create this plan. /// The MSBuild configuration used to discover projects and dependencies. /// Unchanged package projects needed to build selected releases, with their released versions. /// The build working directory relative to the repository root, using forward slashes; . means the root. public sealed record ReleasePlan( IReadOnlyList Packages, - string TargetCommitSha, string Configuration, IReadOnlyList ReferencedPackages, string WorkingDirectory); diff --git a/src/PackageCascade/Public/DependencyInjection/ServiceCollectionExtensions.cs b/src/PackageCascade/Public/DependencyInjection/ServiceCollectionExtensions.cs index 7a61585..8a6f58f 100644 --- a/src/PackageCascade/Public/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/PackageCascade/Public/DependencyInjection/ServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ using PackageCascade.Internal.Executing; using PackageCascade.Internal.Executing.Git; using PackageCascade.Internal.Executing.Packaging; -using PackageCascade.Internal.Git; using PackageCascade.Internal.Planning; using PackageCascade.Internal.Planning.Git; using PackageCascade.Internal.Planning.Projects; @@ -23,12 +22,12 @@ public IServiceCollection AddPackageCascade() { services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/PackageCascade/Public/Executing/ExecutionOptions.cs b/src/PackageCascade/Public/Executing/ExecutionOptions.cs index 6717010..29314ab 100644 --- a/src/PackageCascade/Public/Executing/ExecutionOptions.cs +++ b/src/PackageCascade/Public/Executing/ExecutionOptions.cs @@ -1,6 +1,6 @@ namespace PackageCascade.Public.Executing; /// Options for applying a prepared release plan locally. -/// The directory for generated package archives; it may be created by the executor, and existing files are preserved. +/// The directory for generated package archives. It is created if needed, and archives with matching filenames are replaced. /// Whether to create and verify symbol packages. public sealed record ExecutionOptions(string OutputDirectory, bool IncludeSymbols = false); diff --git a/src/PackageCascade/Public/Executing/IPackageCascadeExecutor.cs b/src/PackageCascade/Public/Executing/IPackageCascadeExecutor.cs index 4ea6cc7..3014922 100644 --- a/src/PackageCascade/Public/Executing/IPackageCascadeExecutor.cs +++ b/src/PackageCascade/Public/Executing/IPackageCascadeExecutor.cs @@ -5,8 +5,8 @@ namespace PackageCascade.Public.Executing; /// Builds and packs the releases in a prepared plan, verifies the archives, then creates local Git tags. public interface IPackageCascadeExecutor { - /// Applies the supplied plan at its target commit without publishing packages or pushing tags. - /// The prepared releases, target commit and build configuration. + /// Builds, verifies and tags the supplied releases without publishing packages or pushing tags. + /// The prepared releases and build configuration. /// Path to the Git repository containing the solution. /// Package output and optional symbol settings. /// Cancels SDK commands and subsequent execution steps.