From 46770a412a6ea6bf8e934b8e0541244634d7f1e9 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 22 Jan 2025 03:02:20 +1000 Subject: [PATCH] Fixes upgrade dependency could use pre-release #2726 (#2727) --- docs/CHANGELOG-v3.md | 6 +++ .../Commands/ModuleCommand.cs | 54 ++++++++++++++++--- .../Resources/Messages.Designer.cs | 2 +- .../Resources/Messages.resx | 2 +- src/PSRule.Types/Data/ModuleConstraint.cs | 16 +++++- src/PSRule.Types/Data/SemanticVersion.cs | 2 +- .../Pipeline/Dependencies/IntegrityBuilder.cs | 3 +- src/PSRule/Pipeline/Dependencies/LockEntry.cs | 2 + .../PSRule.EditorServices.Tests.csproj | 2 +- .../Pipeline/Dependencies/LockFileTests.cs | 10 ++++ tests/PSRule.Tests/test.lock.json | 5 ++ tests/PSRule.Tool.Tests/CommandTests.cs | 2 - .../PSRule.Tool.Tests.csproj | 9 ++-- .../PSRule.Types.Tests.csproj | 2 +- 14 files changed, 96 insertions(+), 21 deletions(-) diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index db3794ba57..5a82df9df6 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -27,6 +27,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v3.0.0-B0390: + +- Bug fixes: + - Fixed upgrade dependency could use pre-release version by @BernieWhite. + [#2726](https://github.com/microsoft/PSRule/issues/2726) + ## v3.0.0-B0390 (pre-release) What's changed since pre-release v3.0.0-B0351: diff --git a/src/PSRule.CommandLine/Commands/ModuleCommand.cs b/src/PSRule.CommandLine/Commands/ModuleCommand.cs index a7141dea7d..639434961c 100644 --- a/src/PSRule.CommandLine/Commands/ModuleCommand.cs +++ b/src/PSRule.CommandLine/Commands/ModuleCommand.cs @@ -277,9 +277,10 @@ public static async Task ModuleAddAsync(ModuleOptions operationOptions, Cli return ERROR_MODULE_FAILED_TO_FIND; } - if (!IsInstalled(pwsh, module, idealVersion, out _, out _)) + if (!IsInstalled(pwsh, module, idealVersion, out _, out _) && await InstallVersionAsync(clientContext, module, idealVersion, null, cancellationToken) == null) { - await InstallVersionAsync(clientContext, module, idealVersion, null, cancellationToken); + clientContext.LogError(Messages.Error_501, module, idealVersion); + return ERROR_MODULE_FAILED_TO_INSTALL; } clientContext.LogVerbose(Messages.UsingModule, module, idealVersion.ToString()); @@ -352,8 +353,17 @@ public static async Task ModuleUpgradeAsync(ModuleOptions operationOptions, using var pwsh = CreatePowerShell(); foreach (var kv in file.Modules.Where(m => filteredModules == null || filteredModules.Contains(m.Key))) { - // Get a constraint if set from options. - var moduleConstraint = requires.TryGetValue(kv.Key, out var c) ? c : ModuleConstraint.Any(kv.Key, includePrerelease: kv.Value.IncludePrerelease ?? operationOptions.Prerelease); + var includePrerelease = kv.Value.IncludePrerelease ?? operationOptions.Prerelease; + + // Get the module constraint. + var moduleConstraint = ModuleConstraint.Any(kv.Key, includePrerelease: includePrerelease); + + // Use the constraint set in options. + if (requires.TryGetValue(kv.Key, out var c)) + { + // Only allow pre-releases if both the constraint and lock file/ context allows it. + moduleConstraint = !includePrerelease ? c.Stable() : c; + } // Find the ideal version. var idealVersion = await FindVersionAsync(kv.Key, moduleConstraint, null, null, cancellationToken); @@ -366,9 +376,10 @@ public static async Task ModuleUpgradeAsync(ModuleOptions operationOptions, if (idealVersion == kv.Value.Version) continue; - if (!IsInstalled(pwsh, kv.Key, idealVersion, out _, out _)) + if (!IsInstalled(pwsh, kv.Key, idealVersion, out _, out _) && await InstallVersionAsync(clientContext, kv.Key, idealVersion, null, cancellationToken) == null) { - await InstallVersionAsync(clientContext, kv.Key, idealVersion, null, cancellationToken); + clientContext.LogError(Messages.Error_501, kv.Key, idealVersion); + return ERROR_MODULE_FAILED_TO_INSTALL; } clientContext.LogVerbose(Messages.UsingModule, kv.Key, idealVersion.ToString()); @@ -580,7 +591,7 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H // Clean up the temp path. if (Directory.Exists(tempPath)) { - Directory.Delete(tempPath, true); + Retry(3, 1000, () => Directory.Delete(tempPath, true)); } context.LogError(Messages.Error_504, name, version); @@ -593,7 +604,7 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H Directory.CreateDirectory(parentDirectory); // Move the module to the final path. - Directory.Move(tempPath, modulePath); + Retry(3, 1000, () => Directory.Move(tempPath, modulePath)); if (!Directory.Exists(modulePath)) return null; @@ -632,5 +643,32 @@ private static PowerShell CreatePowerShell() return PowerShell.Create(); } + /// + /// Retry an action a number of times with a delay. + /// + /// The number of retries. + /// The delay in milliseconds. + /// The action to attempt. + private static void Retry(int retryCount, int delay, Action action) + { + var attempts = 0; + while (attempts < retryCount) + { + try + { + action(); + return; + } + catch (Exception) + { + attempts++; + if (attempts >= retryCount) + throw; + + Thread.Sleep(delay); + } + } + } + #endregion Helper methods } diff --git a/src/PSRule.CommandLine/Resources/Messages.Designer.cs b/src/PSRule.CommandLine/Resources/Messages.Designer.cs index 805f99f232..53b25cc5ce 100644 --- a/src/PSRule.CommandLine/Resources/Messages.Designer.cs +++ b/src/PSRule.CommandLine/Resources/Messages.Designer.cs @@ -61,7 +61,7 @@ internal Messages() { } /// - /// Looks up a localized string similar to Failed to restore module: {0} -- v{1}. + /// Looks up a localized string similar to Failed to install module: {0} -- v{1}. /// internal static string Error_501 { get { diff --git a/src/PSRule.CommandLine/Resources/Messages.resx b/src/PSRule.CommandLine/Resources/Messages.resx index ebe06b28a6..c04a778548 100644 --- a/src/PSRule.CommandLine/Resources/Messages.resx +++ b/src/PSRule.CommandLine/Resources/Messages.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Failed to restore module: {0} -- v{1} + Failed to install module: {0} -- v{1} Failed to find a valid version of the specified module '{0}'. diff --git a/src/PSRule.Types/Data/ModuleConstraint.cs b/src/PSRule.Types/Data/ModuleConstraint.cs index bda9c70b85..0b81958ae0 100644 --- a/src/PSRule.Types/Data/ModuleConstraint.cs +++ b/src/PSRule.Types/Data/ModuleConstraint.cs @@ -14,6 +14,8 @@ namespace PSRule.Data; [DebuggerDisplay("{Module}")] public sealed class ModuleConstraint(string module, ISemanticVersionConstraint constraint) : ISemanticVersionConstraint { + private bool _RequireStableVersions = false; + /// /// The name of the module. /// @@ -25,7 +27,19 @@ public sealed class ModuleConstraint(string module, ISemanticVersionConstraint c public ISemanticVersionConstraint Constraint { get; } = constraint ?? throw new ArgumentNullException(nameof(constraint)); /// - public bool Accepts(SemanticVersion.Version? version) => Constraint.Accepts(version); + public bool Accepts(SemanticVersion.Version? version) + { + return version != null && _RequireStableVersions && !version.Stable ? false : Constraint.Accepts(version); + } + + /// + /// Flag that any accept versions must be stable. + /// + public ModuleConstraint Stable() + { + _RequireStableVersions = true; + return this; + } /// /// Get a constraint that accepts any version of the specified module. diff --git a/src/PSRule.Types/Data/SemanticVersion.cs b/src/PSRule.Types/Data/SemanticVersion.cs index 5ea754bba6..6f80954aa7 100644 --- a/src/PSRule.Types/Data/SemanticVersion.cs +++ b/src/PSRule.Types/Data/SemanticVersion.cs @@ -437,7 +437,7 @@ private string GetExpressionString() /// /// A semantic version. /// - [DebuggerDisplay("{_VersionString}")] + [DebuggerDisplay("{ToString()}")] public sealed class Version : IComparable, IEquatable { private string? _VersionString; diff --git a/src/PSRule/Pipeline/Dependencies/IntegrityBuilder.cs b/src/PSRule/Pipeline/Dependencies/IntegrityBuilder.cs index 70d2a9cc94..d1fc0d947e 100644 --- a/src/PSRule/Pipeline/Dependencies/IntegrityBuilder.cs +++ b/src/PSRule/Pipeline/Dependencies/IntegrityBuilder.cs @@ -33,8 +33,7 @@ private sealed class FileIntegrity(string path, string hash) /// The directory path to the dependency. public static LockEntryIntegrity Build(IntegrityAlgorithm alg, string path) { - if (!Directory.Exists(path)) - throw new InvalidOperationException($"The path '{path}' does not exist."); + if (!Directory.Exists(path)) throw new InvalidOperationException($"The path '{path}' does not exist."); var ignoredFiles = new HashSet(StringComparer.OrdinalIgnoreCase) { diff --git a/src/PSRule/Pipeline/Dependencies/LockEntry.cs b/src/PSRule/Pipeline/Dependencies/LockEntry.cs index 5be47b381c..e325fe3820 100644 --- a/src/PSRule/Pipeline/Dependencies/LockEntry.cs +++ b/src/PSRule/Pipeline/Dependencies/LockEntry.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using Newtonsoft.Json; using PSRule.Converters.Json; using PSRule.Data; @@ -14,6 +15,7 @@ namespace PSRule.Pipeline.Dependencies; /// /// An entry within the lock file. /// +[DebuggerDisplay("{Version.ToString()}, IncludePrerelease={IncludePrerelease}, {Integrity.Hash}")] public sealed class LockEntry(SemanticVersion.Version version) { /// diff --git a/tests/PSRule.EditorServices.Tests/PSRule.EditorServices.Tests.csproj b/tests/PSRule.EditorServices.Tests/PSRule.EditorServices.Tests.csproj index 5c7ce68c0f..31e03be05c 100644 --- a/tests/PSRule.EditorServices.Tests/PSRule.EditorServices.Tests.csproj +++ b/tests/PSRule.EditorServices.Tests/PSRule.EditorServices.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/tests/PSRule.Tests/Pipeline/Dependencies/LockFileTests.cs b/tests/PSRule.Tests/Pipeline/Dependencies/LockFileTests.cs index 39ef6f8eab..414af91e29 100644 --- a/tests/PSRule.Tests/Pipeline/Dependencies/LockFileTests.cs +++ b/tests/PSRule.Tests/Pipeline/Dependencies/LockFileTests.cs @@ -18,10 +18,20 @@ public void Read_WhenValidLockFile_ShouldReturnInstance() Assert.Equal("1.1.0", item.Version.ToString()); Assert.Equal(IntegrityAlgorithm.SHA512, item.Integrity.Algorithm); Assert.Equal("4oEbkAT3VIQQlrDUOpB9qKkbNU5BMktvkDCriws4LgCMUiyUoYMcN0XovljAIW4FO0cmP7mP6A8Z7MPNGlgK7Q==", item.Integrity.Hash); + Assert.Null(item.IncludePrerelease); + // Test string casing. Assert.True(lockFile.Modules.TryGetValue("psrule.rules.msft.oss", out item)); Assert.Equal("1.1.0", item.Version.ToString()); Assert.Equal(IntegrityAlgorithm.SHA512, item.Integrity.Algorithm); Assert.Equal("4oEbkAT3VIQQlrDUOpB9qKkbNU5BMktvkDCriws4LgCMUiyUoYMcN0XovljAIW4FO0cmP7mP6A8Z7MPNGlgK7Q==", item.Integrity.Hash); + Assert.Null(item.IncludePrerelease); + + // Test second module. + Assert.True(lockFile.Modules.TryGetValue("PSRule.Rules.Azure", out item)); + Assert.Equal("1.39.3", item.Version.ToString()); + Assert.Equal(IntegrityAlgorithm.SHA512, item.Integrity.Algorithm); + Assert.Equal("BS6NhS0xlt7+iLoBWchc72I3/LAi1bWum9jV48aKNfQ/02lzrCiSgUHu3Svc0sS3oICdSfO3zoxlcI24Oo3Zfw==", item.Integrity.Hash); + Assert.True(item.IncludePrerelease); } } diff --git a/tests/PSRule.Tests/test.lock.json b/tests/PSRule.Tests/test.lock.json index 201aa6cc83..62ae718384 100644 --- a/tests/PSRule.Tests/test.lock.json +++ b/tests/PSRule.Tests/test.lock.json @@ -4,6 +4,11 @@ "PSRule.Rules.MSFT.OSS": { "version": "1.1.0", "integrity": "sha512-4oEbkAT3VIQQlrDUOpB9qKkbNU5BMktvkDCriws4LgCMUiyUoYMcN0XovljAIW4FO0cmP7mP6A8Z7MPNGlgK7Q==" + }, + "PSRule.Rules.Azure": { + "version": "1.39.3", + "includePrerelease": true, + "integrity": "sha512-BS6NhS0xlt7+iLoBWchc72I3/LAi1bWum9jV48aKNfQ/02lzrCiSgUHu3Svc0sS3oICdSfO3zoxlcI24Oo3Zfw==" } } } diff --git a/tests/PSRule.Tool.Tests/CommandTests.cs b/tests/PSRule.Tool.Tests/CommandTests.cs index 4aaf16c639..bf3868f944 100644 --- a/tests/PSRule.Tool.Tests/CommandTests.cs +++ b/tests/PSRule.Tool.Tests/CommandTests.cs @@ -3,8 +3,6 @@ using System.CommandLine; using System.CommandLine.IO; -using System.Linq; -using System.Threading.Tasks; namespace PSRule.Tool; diff --git a/tests/PSRule.Tool.Tests/PSRule.Tool.Tests.csproj b/tests/PSRule.Tool.Tests/PSRule.Tool.Tests.csproj index 9119d23476..cb22c7a6a2 100644 --- a/tests/PSRule.Tool.Tests/PSRule.Tool.Tests.csproj +++ b/tests/PSRule.Tool.Tests/PSRule.Tool.Tests.csproj @@ -2,11 +2,14 @@ net8.0 - {c1d2bc26-305a-4985-8dc5-177449ff2cfd} - true - false PSRule.Tool + {c1d2bc26-305a-4985-8dc5-177449ff2cfd} Full + 12.0 + enable + enable + false + true diff --git a/tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj b/tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj index 8aa48060d6..d0eef6682f 100644 --- a/tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj +++ b/tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj @@ -4,10 +4,10 @@ net8.0 PSRule {8860178f-4b4a-4e28-8cc3-85dfe2a2fe4b} + Full 12.0 enable enable - false true