diff --git a/.ci/test.yml b/.ci/test.yml index c5a20e40e..cd8c8c88f 100644 --- a/.ci/test.yml +++ b/.ci/test.yml @@ -48,7 +48,7 @@ jobs: - ${{ parameters.powershellExecutable }}: | $modulePath = Join-Path -Path $env:AGENT_TEMPDIRECTORY -ChildPath 'TempModules' Write-Verbose -Verbose "Install Microsoft.PowerShell.PSResourceGet to temp module path" - Save-Module -Name Microsoft.PowerShell.PSResourceGet -MinimumVersion 0.9.0-rc1 -Path $modulePath -AllowPrerelease -Force + Save-Module -Name Microsoft.PowerShell.PSResourceGet -Path $modulePath -Force -Verbose Write-Verbose -Verbose "Install Pester 4.X to temp module path" Save-Module -Name "Pester" -MaximumVersion 4.99 -Path $modulePath -Force displayName: Install Microsoft.PowerShell.PSResourceGet and Pester @@ -59,7 +59,7 @@ jobs: Write-Verbose -Verbose "Importing build utilities (buildtools.psd1)" Import-Module -Name (Join-Path -Path '${{ parameters.buildDirectory }}' -ChildPath 'buildtools.psd1') -Force # - Install-ModulePackageForTest -PackagePath "$(System.ArtifactsDirectory)" + Install-ModulePackageForTest -PackagePath "$(System.ArtifactsDirectory)" -ErrorAction stop -Verbose displayName: Install module for test from downloaded artifact workingDirectory: ${{ parameters.buildDirectory }} diff --git a/.pipelines/PSResourceGet-Official.yml b/.pipelines/PSResourceGet-Official.yml index bc6a10f14..d096cf33e 100644 --- a/.pipelines/PSResourceGet-Official.yml +++ b/.pipelines/PSResourceGet-Official.yml @@ -152,7 +152,7 @@ extends: inputs: command: 'sign' signing_profile: external_distribution - files_to_sign: '**\*.psd1;**\*.psm1;**\*.ps1xml;**\Microsoft*.dll' + files_to_sign: '**\*.ps1;**\*.psd1;**\*.psm1;**\*.ps1xml;**\Microsoft*.dll' search_root: $(signSrcPath) - pwsh: | diff --git a/CHANGELOG/1.0.md b/CHANGELOG/1.0.md index f4956b644..20348dea3 100644 --- a/CHANGELOG/1.0.md +++ b/CHANGELOG/1.0.md @@ -7,6 +7,7 @@ ## [1.0.5](https://github.com/PowerShell/PSResourceGet/compare/v1.0.4.1...v1.0.5) - 2024-05-13 ### Bug Fixes + - Update `nuget.config` to use PowerShell packages feed (#1649) - Refactor V2ServerAPICalls and NuGetServerAPICalls to use object-oriented query/filter builder (#1645 Thanks @sean-r-williams!) - Fix unnecessary `and` for version globbing in V2ServerAPICalls (#1644 Thanks again @sean-r-williams!) @@ -422,7 +423,7 @@ All tests have been reviewed and rewritten as needed. ## 3.0.0-beta8 -### New Features +### New Features - Add `-Type` parameter to `Install-PSResource` - Add 'sudo' check for admin privileges in Unix in `Install-PSResource` @@ -436,7 +437,7 @@ All tests have been reviewed and rewritten as needed. ## 3.0.0-beta7 -### New Features +### New Features - Completed functionality for `Update-PSResource` - `Input-Object` parameter for `Install-PSResource` @@ -499,4 +500,5 @@ All tests have been reviewed and rewritten as needed. ## 3.0.0-beta1 ### BREAKING CHANGE -- Preview version of PowerShellGet. Many features are not fully implemented yet. Please see https://devblogs.microsoft.com/powershell/powershellget-3-0-preview1 for more details. + +- Preview version of PowerShellGet. Many features are not fully implemented yet. Please see for more details. diff --git a/CHANGELOG/1.1.md b/CHANGELOG/1.1.md new file mode 100644 index 000000000..ba6cce61e --- /dev/null +++ b/CHANGELOG/1.1.md @@ -0,0 +1,12 @@ +## [1.1.0](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-rc3...v1.1.0) - 2025-01-09 + +### Bug Fixes + +- Bugfix for publishing .nupkg file to ContainerRegistry repository (#1763) +- Bugfix for PMPs like Artifactory needing modified filter query parameter to proxy upstream (#1761) +- Bugfix for ContainerRegistry repository to parse out dependencies from metadata (#1766) +- Bugfix for Install-PSResource Null pointer occurring when package is present only in upstream feed in ADO (#1760) +- Bugfix for local repository casing issue on Linux (#1750) +- Update README.md (#1759) +- Bug fix for case sensitive License.txt when RequireLicense is specified (#1757) +- Bug fix for broken -Quiet parameter for Save-PSResource (#1745) diff --git a/CHANGELOG/preview.md b/CHANGELOG/preview.md index a27564140..53d1677b2 100644 --- a/CHANGELOG/preview.md +++ b/CHANGELOG/preview.md @@ -1,4 +1,23 @@ -## [1.1.0-RC1](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-preview2...v1.1.0-RC1) - 2024-09-13 +## ## [1.1.0-rc3](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-RC2...v1.1.0-rc3) - 2024-11-15 + +### Bug Fix +- Include missing commits + + +## [1.1.0-RC2](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-RC1...v1.1.0-RC2) - 2024-10-30 + +### New Features +- Full Microsoft Artifact Registry integration (#1741) + +### Bug Fixes + +- Update to use OCI v2 APIs for Container Registry (#1737) +- Bug fixes for finding and installing from local repositories on Linux machines (#1738) +- Bug fix for finding package name with 4 part version from local repositories (#1739) + +# Preview Changelog + +## [1.1.0-RC1](https://github.com/PowerShell/PSResourceGet/compare/v1.1.0-preview2...v1.1.0-RC1) - 2024-10-22 ### New Features @@ -8,9 +27,9 @@ - Fix packaging name matching when searching in local repositories (#1731) - `Compress-PSResource` `-PassThru` now passes `FileInfo` instead of string (#1720) -- Fix for `Compress-PSResource` not properly compressing scripts (#1719) +- Fix for `Compress-PSResource` not properly compressing scripts (#1719) - Add `AcceptLicense` to Save-PSResource (#1718 Thanks @o-l-a-v!) -- Better support for NuGet v2 feeds (#1713 Thanks @o-l-a-v!) +- Better support for Azure DevOps Artifacts NuGet v2 feeds (#1713 Thanks @o-l-a-v!) - Better handling of `-WhatIf` support in `Install-PSResource` (#1531 Thanks @o-l-a-v!) - Fix for some nupkgs failing to extract due to empty directories (#1707 Thanks @o-l-a-v!) - Fix for searching for `-Name *` in `Find-PSResource` (#1706 Thanks @o-l-a-v!) @@ -34,8 +53,7 @@ ### New Features -- Support for Azure Container Registries (#1495, #1497-#1499, #1501, #1502, #1505, #1522, #1545, #1548, #1550, #1554, #1560, #1567, -#1573, #1576, #1587, #1588, #1589, #1594, #1598, #1600, #1602, #1604, #1615) +- Support for Azure Container Registries (#1495, #1497-#1499, #1501, #1502, #1505, #1522, #1545, #1548, #1550, #1554, #1560, #1567, #1573, #1576, #1587, #1588, #1589, #1594, #1598, #1600, #1602, #1604, #1615) ### Bug Fixes diff --git a/README.md b/README.md index 5222ac17a..e94344ae1 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,11 @@ C:\> Import-Module C:\Repos\PSResourceGet\out\PSResourceGet c:\> PowerShell C:\> Import-Module C:\Repos\PSResourceGet\out\PSResourceGet\PSResourceGet.psd1 ``` +## Module Support Lifecycle +Microsoft.PowerShell.PSResourceGet follows the support lifecycle of the version of PowerShell that it ships in. +For example, PSResourceGet 1.0.x shipped in PowerShell 7.4 which is an LTS release so it will be supported for 3 years. +Preview versions of the module, or versions that ship in preview versions of PowerShell are not supported. +Versions of PSResourceGet that do not ship in a version of PowerShell will be fixed forward. ## Code of Conduct diff --git a/buildtools.psm1 b/buildtools.psm1 index 1df564eb1..9aab19832 100644 --- a/buildtools.psm1 +++ b/buildtools.psm1 @@ -120,7 +120,23 @@ function Install-ModulePackageForTest { } Write-Verbose -Verbose -Message "Installing module $($config.ModuleName) to build output path $installationPath" - Save-PSResource -Name $config.ModuleName -Repository $localRepoName -Path $installationPath -SkipDependencyCheck -Prerelease -Confirm:$false -TrustRepository + $psgetModuleBase = (get-command save-psresource).Module.ModuleBase + $psgetVersion = (get-command save-psresource).Module.Version.ToString() + $psgetPrerelease = (get-command find-psresource).module.PrivateData.PSData.Prerelease + Write-Verbose -Verbose -Message "PSResourceGet module base imported: $psgetModuleBase" + Write-Verbose -Verbose -Message "PSResourceGet version base imported: $psgetVersion" + Write-Verbose -Verbose -Message "PSResourceGet prerelease base imported: $psgetPrerelease" + #Save-PSResource -Name $config.ModuleName -Repository $localRepoName -Path $installationPath -SkipDependencyCheck -Prerelease -Confirm:$false -TrustRepository + + Register-PSRepository -Name $localRepoName -SourceLocation $packagePathWithNupkg -InstallationPolicy Trusted -Verbose + $psgetv2ModuleBase = (get-command save-module).Module.ModuleBase + $psgetv2Version = (get-command save-module).Module.Version.ToString() + $psgetv2Prerelease = (get-command save-module).module.PrivateData.PSData.Prerelease + Write-Verbose -Verbose -Message "PowerShellGet module base imported: $psgetv2ModuleBase" + Write-Verbose -Verbose -Message "PowerShellGet version base imported: $psgetv2Version" + Write-Verbose -Verbose -Message "PowerShellGet prerelease base imported: $psgetv2Prerelease" + Save-Module -Name $config.ModuleName -Repository $localRepoName -Path $installationPath -Force -Verbose -AllowPrerelease -Confirm:$false + Unregister-PSRepository -Name $localRepoName Write-Verbose -Verbose -Message "Unregistering local package repo: $localRepoName" Unregister-PSResourceRepository -Name $localRepoName -Confirm:$false diff --git a/global.json b/global.json index 120c43985..b832c3a01 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.403" + "version": "8.0.404" } } diff --git a/src/Microsoft.PowerShell.PSResourceGet.psd1 b/src/Microsoft.PowerShell.PSResourceGet.psd1 index 3a497db4e..30994ba2e 100644 --- a/src/Microsoft.PowerShell.PSResourceGet.psd1 +++ b/src/Microsoft.PowerShell.PSResourceGet.psd1 @@ -46,7 +46,7 @@ 'udres') PrivateData = @{ PSData = @{ - Prerelease = 'RC1' + # Prerelease = '' Tags = @('PackageManagement', 'PSEdition_Desktop', 'PSEdition_Core', @@ -56,6 +56,34 @@ ProjectUri = 'https://go.microsoft.com/fwlink/?LinkId=828955' LicenseUri = 'https://go.microsoft.com/fwlink/?LinkId=829061' ReleaseNotes = @' +## 1.1.0 + +### Bug Fix +- Bugfix for publishing .nupkg file to ContainerRegistry repository (#1763) +- Bugfix for PMPs like Artifactory needing modified filter query parameter to proxy upstream (#1761) +- Bugfix for ContainerRegistry repository to parse out dependencies from metadata (#1766) +- Bugfix for Install-PSResource Null pointer occurring when package is present only in upstream feed in ADO (#1760) +- Bugfix for local repository casing issue on Linux (#1750) +- Update README.md (#1759) +- Bug fix for case sensitive License.txt when RequireLicense is specified (#1757) +- Bug fix for broken -Quiet parameter for Save-PSResource (#1745) + +## 1.1.0-rc3 + +### Bug Fix +- Include missing commits + +## 1.1.0-RC2 + +### New Features +- Full Microsoft Artifact Registry integration (#1741) + +### Bug Fixes + +- Update to use OCI v2 APIs for Container Registry (#1737) +- Bug fixes for finding and installing from local repositories on Linux machines (#1738) +- Bug fix for finding package name with 4 part version from local repositories (#1739) + ## 1.1.0-RC1 ### New Features diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index f5310d38c..d9175c0b0 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -43,7 +43,7 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryOAuthTokenUrlTemplate = "https://{0}/oauth2/token"; // 0 - registry const string containerRegistryManifestUrlTemplate = "https://{0}/v2/{1}/manifests/{2}"; // 0 - registry, 1 - repo(modulename), 2 - tag(version) const string containerRegistryBlobDownloadUrlTemplate = "https://{0}/v2/{1}/blobs/{2}"; // 0 - registry, 1 - repo(modulename), 2 - layer digest - const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/acr/v1/{1}/_tags{2}"; // 0 - registry, 1 - repo(modulename), 2 - /tag(version) + const string containerRegistryFindImageVersionUrlTemplate = "https://{0}/v2/{1}/tags/list"; // 0 - registry, 1 - repo(modulename) const string containerRegistryStartUploadTemplate = "https://{0}/v2/{1}/blobs/uploads/"; // 0 - registry, 1 - packagename const string containerRegistryEndUploadTemplate = "https://{0}{1}&digest=sha256:{2}"; // 0 - registry, 1 - location, 2 - digest @@ -286,7 +286,8 @@ public override Stream InstallPackage(string packageName, string packageVersion, return results; } - results = InstallVersion(packageName, packageVersion, out errRecord); + string packageNameForInstall = PrependMARPrefix(packageName); + results = InstallVersion(packageNameForInstall, packageVersion, out errRecord); return results; } @@ -413,6 +414,7 @@ internal string GetContainerRegistryAccessToken(out ErrorRecord errRecord) else { _cmdletPassedIn.WriteVerbose("Repository is unauthenticated"); + return null; } } @@ -572,27 +574,19 @@ internal async Task GetContainerRegistryBlobAsync(string packageNam /// internal JObject FindContainerRegistryImageTags(string packageName, string version, string containerRegistryAccessToken, out ErrorRecord errRecord) { - /* response returned looks something like: - * "registry": "myregistry.azurecr.io" - * "imageName": "hello-world" - * "tags": [ - * { - * ""name"": ""1.0.0"", - * ""digest"": ""sha256:92c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a"", - * ""createdTime"": ""2023-12-23T18:06:48.9975733Z"", - * ""lastUpdateTime"": ""2023-12-23T18:06:48.9975733Z"", - * ""signed"": false, - * ""changeableAttributes"": { - * ""deleteEnabled"": true, - * ""writeEnabled"": true, - * ""readEnabled"": true, - * ""listEnabled"": true - * } - * }] - */ + /* + { + "name": "", + "tags": [ + "", + "", + "" + ] + } + */ _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindContainerRegistryImageTags()"); string resolvedVersion = string.Equals(version, "*", StringComparison.OrdinalIgnoreCase) ? null : $"/{version}"; - string findImageUrl = string.Format(containerRegistryFindImageVersionUrlTemplate, Registry, packageName, resolvedVersion); + string findImageUrl = string.Format(containerRegistryFindImageVersionUrlTemplate, Registry, packageName); var defaultHeaders = GetDefaultHeaders(containerRegistryAccessToken); return GetHttpResponseJObjectUsingDefaultHeaders(findImageUrl, HttpMethod.Get, defaultHeaders, out errRecord); } @@ -655,9 +649,9 @@ internal Hashtable GetContainerRegistryMetadata(string packageName, string exact pkgVersionString += $"-{pkgPrereleaseLabelElement.ToString()}"; } } - else if (rootDom.TryGetProperty("Version", out pkgVersionElement)) + else if (rootDom.TryGetProperty("Version", out pkgVersionElement) || rootDom.TryGetProperty("version", out pkgVersionElement)) { - // script metadata will have "Version" property + // script metadata will have "Version" property, but nupkg only based .nuspec will have lowercase "version" property and JsonElement.TryGetProperty() is case sensitive pkgVersionString = pkgVersionElement.ToString(); } else @@ -1121,12 +1115,11 @@ private static Collection> GetDefaultHeaders(string #endregion #region Publish Methods - /// /// Helper method that publishes a package to the container registry. /// This gets called from Publish-PSResource. /// - internal bool PushNupkgContainerRegistry(string psd1OrPs1File, + internal bool PushNupkgContainerRegistry( string outputNupkgDir, string packageName, string modulePrefix, @@ -1134,10 +1127,14 @@ internal bool PushNupkgContainerRegistry(string psd1OrPs1File, ResourceType resourceType, Hashtable parsedMetadataHash, Hashtable dependencies, + bool isNupkgPathSpecified, + string originalNupkgPath, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::PushNupkgContainerRegistry()"); - string fullNupkgFile = System.IO.Path.Combine(outputNupkgDir, packageName + "." + packageVersion.ToNormalizedString() + ".nupkg"); + + // if isNupkgPathSpecified, then we need to publish the original .nupkg file, as it may be signed + string fullNupkgFile = isNupkgPathSpecified ? originalNupkgPath : System.IO.Path.Combine(outputNupkgDir, packageName + "." + packageVersion.ToNormalizedString() + ".nupkg"); string pkgNameForUpload = string.IsNullOrEmpty(modulePrefix) ? packageName : modulePrefix + "/" + packageName; string packageNameLowercase = pkgNameForUpload.ToLower(); @@ -1608,13 +1605,14 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp string registryUrl = Repository.Uri.ToString(); string packageNameLowercase = packageName.ToLower(); + string packageNameForFind = PrependMARPrefix(packageNameLowercase); string containerRegistryAccessToken = GetContainerRegistryAccessToken(out errRecord); if (errRecord != null) { return emptyHashResponses; } - var foundTags = FindContainerRegistryImageTags(packageNameLowercase, "*", containerRegistryAccessToken, out errRecord); + var foundTags = FindContainerRegistryImageTags(packageNameForFind, "*", containerRegistryAccessToken, out errRecord); if (errRecord != null || foundTags == null) { return emptyHashResponses; @@ -1623,7 +1621,7 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp List latestVersionResponse = new List(); List allVersionsList = foundTags["tags"].ToList(); - SortedDictionary sortedQualifyingPkgs = GetPackagesWithRequiredVersion(allVersionsList, versionType, versionRange, requiredVersion, packageNameLowercase, includePrerelease, out errRecord); + SortedDictionary sortedQualifyingPkgs = GetPackagesWithRequiredVersion(allVersionsList, versionType, versionRange, requiredVersion, packageNameForFind, includePrerelease, out errRecord); if (errRecord != null) { return emptyHashResponses; @@ -1634,7 +1632,7 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp foreach (var pkgVersionTag in pkgsInDescendingOrder) { string exactTagVersion = pkgVersionTag.Value.ToString(); - Hashtable metadata = GetContainerRegistryMetadata(packageNameLowercase, exactTagVersion, containerRegistryAccessToken, out errRecord); + Hashtable metadata = GetContainerRegistryMetadata(packageNameForFind, exactTagVersion, containerRegistryAccessToken, out errRecord); if (errRecord != null || metadata.Count == 0) { return emptyHashResponses; @@ -1664,51 +1662,36 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp foreach (var pkgVersionTagInfo in allPkgVersions) { - using (JsonDocument pkgVersionEntry = JsonDocument.Parse(pkgVersionTagInfo.ToString())) + string pkgVersionString = pkgVersionTagInfo.ToString(); + // determine if the package version that is a repository tag is a valid NuGetVersion + if (!NuGetVersion.TryParse(pkgVersionString, out NuGetVersion pkgVersion)) { - JsonElement rootDom = pkgVersionEntry.RootElement; - if (!rootDom.TryGetProperty("name", out JsonElement pkgVersionElement)) - { - errRecord = new ErrorRecord( - new InvalidOrEmptyResponse($"Response does not contain version element ('name') for package '{packageName}' in '{Repository.Name}'."), - "FindNameFailure", - ErrorCategory.InvalidResult, - this); - - return null; - } - - string pkgVersionString = pkgVersionElement.ToString(); - // determine if the package version that is a repository tag is a valid NuGetVersion - if (!NuGetVersion.TryParse(pkgVersionString, out NuGetVersion pkgVersion)) - { - errRecord = new ErrorRecord( - new ArgumentException($"Version {pkgVersionString} to be parsed from metadata is not a valid NuGet version."), - "FindNameFailure", - ErrorCategory.InvalidArgument, - this); + errRecord = new ErrorRecord( + new ArgumentException($"Version {pkgVersionString} to be parsed from metadata is not a valid NuGet version."), + "FindNameFailure", + ErrorCategory.InvalidArgument, + this); - return null; - } + return null; + } - _cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'"); + _cmdletPassedIn.WriteDebug($"'{packageName}' version parsed as '{pkgVersion}'"); - if (isSpecificVersionSearch) + if (isSpecificVersionSearch) + { + if (pkgVersion.ToNormalizedString() == specificVersion.ToNormalizedString()) { - if (pkgVersion.ToNormalizedString() == specificVersion.ToNormalizedString()) - { - // accounts for FindVersion() scenario - sortedPkgs.Add(pkgVersion, pkgVersionString); - break; - } + // accounts for FindVersion() scenario + sortedPkgs.Add(pkgVersion, pkgVersionString); + break; } - else + } + else + { + if (versionRange.Satisfies(pkgVersion) && (!pkgVersion.IsPrerelease || includePrerelease)) { - if (versionRange.Satisfies(pkgVersion) && (!pkgVersion.IsPrerelease || includePrerelease)) - { - // accounts for FindVersionGlobbing() and FindName() scenario - sortedPkgs.Add(pkgVersion, pkgVersionString); - } + // accounts for FindVersionGlobbing() and FindName() scenario + sortedPkgs.Add(pkgVersion, pkgVersionString); } } } @@ -1716,6 +1699,18 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp return sortedPkgs; } + private string PrependMARPrefix(string packageName) + { + string prefix = string.IsNullOrEmpty(InternalHooks.MARPrefix) ? PSRepositoryInfo.MARPrefix : InternalHooks.MARPrefix; + + // If the repostitory is MAR and its not a wildcard search, we need to prefix the package name with MAR prefix. + string updatedPackageName = Repository.IsMARRepository() && packageName.Trim() != "*" + ? string.Concat(prefix, packageName) + : packageName; + + return updatedPackageName; + } + #endregion } } diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 0bdf8b38d..d9d985e9e 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -1336,7 +1336,6 @@ private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string t _cmdletPassedIn.WriteDebug("In InstallHelper::CallAcceptLicense()"); error = null; var requireLicenseAcceptance = false; - var success = true; if (File.Exists(moduleManifest)) { @@ -1364,23 +1363,44 @@ private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string t if (!_acceptLicense) { var PkgTempInstallPath = Path.Combine(tempInstallPath, p.Name, newVersion); - var LicenseFilePath = Path.Combine(PkgTempInstallPath, "License.txt"); + if (!Directory.Exists(PkgTempInstallPath)) + { + error = new ErrorRecord( + new ArgumentException($"Package '{p.Name}' could not be installed: Temporary installation path does not exist."), + "TempPathNotFound", + ErrorCategory.ObjectNotFound, + _cmdletPassedIn); + + return false; + } + + string[] files = Directory.GetFiles(PkgTempInstallPath); + + bool foundLicense = false; + string LicenseFilePath = string.Empty; + foreach (string file in files) + { + if (string.Equals(Path.GetFileName(file), "License.txt", StringComparison.OrdinalIgnoreCase)) + { + foundLicense = true; + LicenseFilePath = Path.GetFullPath(file); + break; + } + } - if (!File.Exists(LicenseFilePath)) + if (!foundLicense) { error = new ErrorRecord( new ArgumentException($"Package '{p.Name}' could not be installed: License.txt not found. License.txt must be provided when user license acceptance is required."), "LicenseTxtNotFound", ErrorCategory.ObjectNotFound, _cmdletPassedIn); - ; - success = false; - return success; + return false; } // Otherwise read LicenseFile - string licenseText = System.IO.File.ReadAllText(LicenseFilePath); + string licenseText = File.ReadAllText(LicenseFilePath); var acceptanceLicenseQuery = $"Do you accept the license terms for module '{p.Name}'?"; var message = licenseText + "\r\n" + acceptanceLicenseQuery; @@ -1403,12 +1423,13 @@ private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string t "ForceAcceptLicense", ErrorCategory.InvalidArgument, _cmdletPassedIn); - success = false; + + return false; } } } - return success; + return true; } /// diff --git a/src/code/InternalHooks.cs b/src/code/InternalHooks.cs index 2078d1d41..0578485ca 100644 --- a/src/code/InternalHooks.cs +++ b/src/code/InternalHooks.cs @@ -15,6 +15,8 @@ public class InternalHooks internal static string AllowedUri; + internal static string MARPrefix; + public static void SetTestHook(string property, object value) { var fieldInfo = typeof(InternalHooks).GetField(property, BindingFlags.Static | BindingFlags.NonPublic); diff --git a/src/code/LocalServerApiCalls.cs b/src/code/LocalServerApiCalls.cs index b22b3efb6..324db1081 100644 --- a/src/code/LocalServerApiCalls.cs +++ b/src/code/LocalServerApiCalls.cs @@ -260,29 +260,18 @@ private FindResults FindNameHelper(string packageName, string[] tags, bool inclu string actualPkgName = packageName; // this regex pattern matches packageName followed by a version (4 digit or 3 with prerelease word) - string regexPattern = $"{packageName}" + @".\d+\.\d+\.\d+(?:-\w+|.\d)*.nupkg"; - Regex rx = new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + string regexPattern = $"{packageName}" + @"(\.\d+){1,3}(?:[a-zA-Z0-9-.]+|.\d)?\.nupkg"; _cmdletPassedIn.WriteDebug($"package file name pattern to be searched for is: {regexPattern}"); foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) { string packageFullName = Path.GetFileName(path); - MatchCollection matches = rx.Matches(packageFullName); - if (matches.Count == 0) - { - continue; - } - - Match match = matches[0]; - - GroupCollection groups = match.Groups; - if (groups.Count == 0) + bool isMatch = Regex.IsMatch(packageFullName, regexPattern, RegexOptions.IgnoreCase); + if (!isMatch) { continue; } - Capture group = groups[0]; - NuGetVersion nugetVersion = GetInfoFromFileName(packageFullName: packageFullName, packageName: packageName, actualName: out actualPkgName, out errRecord); _cmdletPassedIn.WriteDebug($"Version parsed as '{nugetVersion}'"); @@ -389,7 +378,6 @@ private FindResults FindVersionHelper(string packageName, string version, string // this regex pattern matches packageName followed by the requested version string regexPattern = $"{packageName}.{requiredVersion.ToNormalizedString()}" + @".nupkg"; - Regex rx = new Regex(regexPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); _cmdletPassedIn.WriteDebug($"pattern is: {regexPattern}"); string pkgPath = String.Empty; string actualPkgName = String.Empty; @@ -397,22 +385,12 @@ private FindResults FindVersionHelper(string packageName, string version, string foreach (string path in Directory.GetFiles(Repository.Uri.LocalPath)) { string packageFullName = Path.GetFileName(path); - MatchCollection matches = rx.Matches(packageFullName); - if (matches.Count == 0) - { - continue; - } - - Match match = matches[0]; - - GroupCollection groups = match.Groups; - if (groups.Count == 0) + bool isMatch = Regex.IsMatch(packageFullName, regexPattern, RegexOptions.IgnoreCase); + if (!isMatch) { continue; } - Capture group = groups[0]; - NuGetVersion nugetVersion = GetInfoFromFileName(packageFullName: packageFullName, packageName: packageName, actualName: out actualPkgName, out errRecord); _cmdletPassedIn.WriteDebug($"Version parsed as '{nugetVersion}'"); @@ -425,7 +403,7 @@ private FindResults FindVersionHelper(string packageName, string version, string { _cmdletPassedIn.WriteDebug("Found matching version"); string pkgFullName = $"{actualPkgName}.{nugetVersion.ToString()}.nupkg"; - pkgPath = Path.Combine(Repository.Uri.LocalPath, pkgFullName); + pkgPath = path; break; } } @@ -685,7 +663,7 @@ private Hashtable GetMetadataFromNupkg(string packageName, string packagePath, s string psd1FilePath = String.Empty; string ps1FilePath = String.Empty; string nuspecFilePath = String.Empty; - Utils.GetMetadataFilesFromPath(tempDiscoveryPath, packageName, out psd1FilePath, out ps1FilePath, out nuspecFilePath); + Utils.GetMetadataFilesFromPath(tempDiscoveryPath, packageName, out psd1FilePath, out ps1FilePath, out nuspecFilePath, out string properCasingPkgName); List pkgTags = new List(); @@ -710,7 +688,7 @@ private Hashtable GetMetadataFromNupkg(string packageName, string packagePath, s pkgMetadata.Add("ProjectUri", projectUri); pkgMetadata.Add("IconUri", iconUri); pkgMetadata.Add("ReleaseNotes", releaseNotes); - pkgMetadata.Add("Id", packageName); + pkgMetadata.Add("Id", properCasingPkgName); pkgMetadata.Add(_fileTypeKey, Utils.MetadataFileType.ModuleManifest); pkgTags.AddRange(pkgHashTags); @@ -730,7 +708,7 @@ private Hashtable GetMetadataFromNupkg(string packageName, string packagePath, s } pkgMetadata = parsedScript.ToHashtable(); - pkgMetadata.Add("Id", packageName); + pkgMetadata.Add("Id", properCasingPkgName); pkgMetadata.Add(_fileTypeKey, Utils.MetadataFileType.ScriptFile); pkgTags.AddRange(pkgMetadata["Tags"] as string[]); @@ -916,7 +894,7 @@ private NuGetVersion GetInfoFromFileName(string packageFullName, string packageN string[] packageWithoutName = packageFullName.ToLower().Split(new string[]{ $"{packageName.ToLower()}." }, StringSplitOptions.RemoveEmptyEntries); string packageVersionAndExtension = packageWithoutName[0]; - string[] originalFileNameParts = packageFullName.Split(new string[]{ $".{packageVersionAndExtension}" }, StringSplitOptions.RemoveEmptyEntries); + string[] originalFileNameParts = packageFullName.ToLower().Split(new string[]{ $".{packageVersionAndExtension.ToLower()}" }, StringSplitOptions.RemoveEmptyEntries); actualName = String.IsNullOrEmpty(originalFileNameParts[0]) ? packageName : originalFileNameParts[0]; int extensionDot = packageVersionAndExtension.LastIndexOf('.'); string version = packageVersionAndExtension.Substring(0, extensionDot); diff --git a/src/code/PSRepositoryInfo.cs b/src/code/PSRepositoryInfo.cs index b660c6690..b74d52cff 100644 --- a/src/code/PSRepositoryInfo.cs +++ b/src/code/PSRepositoryInfo.cs @@ -11,6 +11,10 @@ namespace Microsoft.PowerShell.PSResourceGet.UtilClasses /// public sealed class PSRepositoryInfo { + #region constants + internal const string MARPrefix = "psresource/"; + #endregion + #region Enums public enum APIVersion @@ -95,5 +99,14 @@ public enum RepositoryProviderType public bool IsAllowedByPolicy { get; set; } #endregion + + #region Methods + + internal bool IsMARRepository() + { + return (ApiVersion == APIVersion.ContainerRegistry && Uri.Host.Contains("mcr.microsoft.com")); + } + + #endregion } } diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 544dcced8..74e482ad3 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -870,7 +870,8 @@ public static bool TryConvertFromContainerRegistryJson( // Version // For scripts (i.e with "Version" property) the version can contain prerelease label - if (rootDom.TryGetProperty("Version", out JsonElement scriptVersionElement)) + // For nupkg only based packages the .nuspec's metadata attributes will be lowercase + if (rootDom.TryGetProperty("Version", out JsonElement scriptVersionElement) || rootDom.TryGetProperty("version", out scriptVersionElement)) { versionValue = scriptVersionElement.ToString(); pkgVersion = ParseHttpVersion(versionValue, out string prereleaseLabel); @@ -917,25 +918,25 @@ public static bool TryConvertFromContainerRegistryJson( metadata["NormalizedVersion"] = parsedNormalizedVersion.ToNormalizedString(); // License Url - if (rootDom.TryGetProperty("LicenseUrl", out JsonElement licenseUrlElement)) + if (rootDom.TryGetProperty("LicenseUrl", out JsonElement licenseUrlElement) || rootDom.TryGetProperty("licenseUrl", out licenseUrlElement)) { metadata["LicenseUrl"] = ParseHttpUrl(licenseUrlElement.ToString()) as Uri; } // Project Url - if (rootDom.TryGetProperty("ProjectUrl", out JsonElement projectUrlElement)) + if (rootDom.TryGetProperty("ProjectUrl", out JsonElement projectUrlElement) || rootDom.TryGetProperty("projectUrl", out projectUrlElement)) { metadata["ProjectUrl"] = ParseHttpUrl(projectUrlElement.ToString()) as Uri; } // Icon Url - if (rootDom.TryGetProperty("IconUrl", out JsonElement iconUrlElement)) + if (rootDom.TryGetProperty("IconUrl", out JsonElement iconUrlElement) || rootDom.TryGetProperty("iconUrl", out iconUrlElement)) { metadata["IconUrl"] = ParseHttpUrl(iconUrlElement.ToString()) as Uri; } // Tags - if (rootDom.TryGetProperty("Tags", out JsonElement tagsElement)) + if (rootDom.TryGetProperty("Tags", out JsonElement tagsElement) || rootDom.TryGetProperty("tags", out tagsElement)) { string[] pkgTags = Utils.EmptyStrArray; if (tagsElement.ValueKind == JsonValueKind.Array) @@ -971,7 +972,7 @@ public static bool TryConvertFromContainerRegistryJson( } // Author - if (rootDom.TryGetProperty("Authors", out JsonElement authorsElement)) + if (rootDom.TryGetProperty("Authors", out JsonElement authorsElement) || rootDom.TryGetProperty("authors", out authorsElement)) { metadata["Authors"] = authorsElement.ToString(); @@ -982,19 +983,19 @@ public static bool TryConvertFromContainerRegistryJson( } // Copyright - if (rootDom.TryGetProperty("Copyright", out JsonElement copyrightElement)) + if (rootDom.TryGetProperty("Copyright", out JsonElement copyrightElement) || rootDom.TryGetProperty("copyright", out copyrightElement)) { metadata["Copyright"] = copyrightElement.ToString(); } // Description - if (rootDom.TryGetProperty("Description", out JsonElement descriptiontElement)) + if (rootDom.TryGetProperty("Description", out JsonElement descriptiontElement) || rootDom.TryGetProperty("description", out descriptiontElement)) { metadata["Description"] = descriptiontElement.ToString(); } // ReleaseNotes - if (rootDom.TryGetProperty("ReleaseNotes", out JsonElement releaseNotesElement)) + if (rootDom.TryGetProperty("ReleaseNotes", out JsonElement releaseNotesElement) || rootDom.TryGetProperty("releaseNotes", out releaseNotesElement)) { metadata["ReleaseNotes"] = releaseNotesElement.ToString(); } @@ -1004,13 +1005,18 @@ public static bool TryConvertFromContainerRegistryJson( { metadata["Dependencies"] = ParseContainerRegistryDependencies(requiredModulesElement, out errorMsg).ToArray(); } + if (string.Equals(packageName, "Az", StringComparison.OrdinalIgnoreCase) || packageName.StartsWith("Az.", StringComparison.OrdinalIgnoreCase)) { - if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) + if (rootDom.TryGetProperty("ModuleList", out JsonElement moduleListDepsElement)) + { + metadata["Dependencies"] = ParseContainerRegistryDependencies(moduleListDepsElement, out errorMsg).ToArray(); + } + else if (rootDom.TryGetProperty("PrivateData", out JsonElement privateDataElement) && privateDataElement.TryGetProperty("PSData", out JsonElement psDataElement)) { - if (psDataElement.TryGetProperty("ModuleList", out JsonElement moduleListDepsElement)) + if (psDataElement.TryGetProperty("ModuleList", out JsonElement privateDataModuleListDepsElement)) { - metadata["Dependencies"] = ParseContainerRegistryDependencies(moduleListDepsElement, out errorMsg).ToArray(); + metadata["Dependencies"] = ParseContainerRegistryDependencies(privateDataModuleListDepsElement, out errorMsg).ToArray(); } } } diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 5470da611..66daea84e 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Management.Automation; using System.Net; using System.Net.Http; @@ -368,6 +369,17 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe return; } + if (repository.IsMARRepository()) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new PSInvalidOperationException($"Repository '{repository.Name}' is a MAR repository and cannot be published to."), + "MARRepositoryPublishError", + ErrorCategory.PermissionDenied, + this)); + + return; + } + _networkCredential = Utils.SetNetworkCredential(repository, _networkCredential, _cmdletPassedIn); // Check if dependencies already exist within the repo if: @@ -429,12 +441,28 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe { ContainerRegistryServerAPICalls containerRegistryServer = new ContainerRegistryServerAPICalls(repository, _cmdletPassedIn, _networkCredential, userAgentString); - var pkgMetadataFile = (resourceType == ResourceType.Script) ? pathToScriptFileToPublish : pathToModuleManifestToPublish; + if (_isNupkgPathSpecified) + { + // copy the .nupkg to a temp path (outputNupkgDir field) as we don't want to tamper with the original, possibly signed, .nupkg file + string copiedNupkgFilePath = CopyNupkgFileToTempPath(nupkgFilePath: Path, errRecord: out ErrorRecord copyErrRecord); + if (copyErrRecord != null) + { + _cmdletPassedIn.WriteError(copyErrRecord); + return; + } + + // get package info (name, version, metadata hashtable) from the copied .nupkg package and then populate appropriate fields (_pkgName, _pkgVersion, parsedMetadata) + GetPackageInfoFromNupkg(nupkgFilePath: copiedNupkgFilePath, errRecord: out ErrorRecord pkgInfoErrRecord); + if (pkgInfoErrRecord != null) + { + _cmdletPassedIn.WriteError(pkgInfoErrRecord); + return; + } + } - if (!containerRegistryServer.PushNupkgContainerRegistry(pkgMetadataFile, outputNupkgDir, _pkgName, modulePrefix, _pkgVersion, resourceType, parsedMetadata, dependencies, out ErrorRecord pushNupkgContainerRegistryError)) + if (!containerRegistryServer.PushNupkgContainerRegistry(outputNupkgDir, _pkgName, modulePrefix, _pkgVersion, resourceType, parsedMetadata, dependencies, _isNupkgPathSpecified, Path, out ErrorRecord pushNupkgContainerRegistryError)) { _cmdletPassedIn.WriteError(pushNupkgContainerRegistryError); - // exit out of processing return; } } @@ -444,6 +472,7 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe { outputNupkgDir = pathToNupkgToPublish; } + // This call does not throw any exceptions, but it will write unsuccessful responses to the console if (!PushNupkg(outputNupkgDir, repository.Name, repository.Uri.ToString(), out ErrorRecord pushNupkgError)) { @@ -463,7 +492,8 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe } finally { - if (!_isNupkgPathSpecified) + // For scenarios such as Publish-PSResource -NupkgPath -Repository , the outputNupkgDir will be set to NupkgPath path, and a temp outputDir folder will not have been created and thus doesn't need to attempt to be deleted + if (Directory.Exists(outputDir)) { _cmdletPassedIn.WriteVerbose(string.Format("Deleting temporary directory '{0}'", outputDir)); Utils.DeleteDirectory(outputDir); @@ -1232,6 +1262,191 @@ private bool CheckDependenciesExist(Hashtable dependencies, string repositoryNam return true; } + /// + /// This method is called by Publish-PSResource when the -NupkgPath parameter is specified + /// The method copies the .nupkg file to a temp path (populated at outputNupkgDir field) as we dont' want to extract and read original .nupkg file + /// + private string CopyNupkgFileToTempPath(string nupkgFilePath, out ErrorRecord errRecord) + { + errRecord = null; + string destinationFilePath = String.Empty; + var packageFullName = System.IO.Path.GetFileName(nupkgFilePath); + try + { + if (!Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + if (!Directory.Exists(outputNupkgDir)) + { + Directory.CreateDirectory(outputNupkgDir); + } + } + + destinationFilePath = System.IO.Path.Combine(outputNupkgDir, packageFullName); + File.Copy(Path, destinationFilePath); + } + catch (Exception e) + { + errRecord = new ErrorRecord( + new ArgumentException($"Error moving .nupkg at -NupkgPath to temp nupkg dir path '{outputNupkgDir}' due to: '{e.Message}'."), + "ErrorMovingNupkg", + ErrorCategory.NotSpecified, + this); + + // exit process record + return destinationFilePath; + } + + return destinationFilePath; + } + + /// + /// Get package info from the .nupkg file provided, inluding package name (_pkgName), package version (_pkgVersion), and metadata parsed into a hashtable (parsedMetadata) + /// + private void GetPackageInfoFromNupkg(string nupkgFilePath, out ErrorRecord errRecord) + { + errRecord = null; + Regex rx = new Regex(@"\.\d+\.", RegexOptions.Compiled | RegexOptions.IgnoreCase); + var packageFullName = System.IO.Path.GetFileName(nupkgFilePath); + MatchCollection matches = rx.Matches(packageFullName); + if (matches.Count == 0) + { + return; + } + + Match match = matches[0]; + + GroupCollection groups = match.Groups; + if (groups.Count == 0) + { + return; + } + + Capture group = groups[0]; + + string pkgFoundName = packageFullName.Substring(0, group.Index); + + string version = packageFullName.Substring(group.Index + 1, packageFullName.LastIndexOf('.') - group.Index - 1); + _cmdletPassedIn.WriteDebug($"Found package '{pkgFoundName}', version '{version}', from packageFullName '{packageFullName}' at path '{Path}'"); + + if (!NuGetVersion.TryParse(version, out NuGetVersion nugetVersion)) + { + errRecord = new ErrorRecord( + new ArgumentException($"Error parsing version '{version}' into NuGetVersion instance."), + "ErrorParsingNuGetVersion", + ErrorCategory.NotSpecified, + this); + + return; + } + + _pkgName = pkgFoundName; + _pkgVersion = nugetVersion; + parsedMetadata = GetMetadataFromNupkg(nupkgFilePath, _pkgName, out errRecord); + } + + /// + /// Extract copied .nupkg, find metadata file (either .ps1, .psd1, or .nuspec) and read metadata into a hashtable + /// + internal Hashtable GetMetadataFromNupkg(string copiedNupkgPath, string packageName, out ErrorRecord errRecord) + { + Hashtable pkgMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase); + errRecord = null; + + // in temp directory create an "extract" folder to which we'll copy .nupkg to, extract contents, etc. + string nupkgDirPath = Directory.GetParent(copiedNupkgPath).FullName; //someGuid/nupkg/myPkg.nupkg -> /someGuid/nupkg + string tempPath = Directory.GetParent(nupkgDirPath).FullName; // someGuid + var extractPath = System.IO.Path.Combine(tempPath, "extract"); // someGuid/extract + + try + { + var dir = Directory.CreateDirectory(extractPath); + dir.Attributes &= ~FileAttributes.ReadOnly; + + // change extension to .zip + string zipFilePath = System.IO.Path.ChangeExtension(copiedNupkgPath, ".zip"); + File.Move(copiedNupkgPath, zipFilePath); + + // extract from .zip + _cmdletPassedIn.WriteDebug($"Extracting '{zipFilePath}' to '{extractPath}'"); + System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, extractPath); + + string psd1FilePath = String.Empty; + string ps1FilePath = String.Empty; + string nuspecFilePath = String.Empty; + Utils.GetMetadataFilesFromPath(extractPath, packageName, out psd1FilePath, out ps1FilePath, out nuspecFilePath, out string properCasingPkgName); + + List pkgTags = new List(); + + if (File.Exists(psd1FilePath)) + { + _cmdletPassedIn.WriteDebug($"Attempting to read module manifest file '{psd1FilePath}'"); + if (!Utils.TryReadManifestFile(psd1FilePath, out pkgMetadata, out Exception readManifestError)) + { + errRecord = new ErrorRecord( + readManifestError, + "GetMetadataFromNupkgFailure", + ErrorCategory.ParserError, + this); + + return pkgMetadata; + } + } + else if (File.Exists(ps1FilePath)) + { + _cmdletPassedIn.WriteDebug($"Attempting to read script file '{ps1FilePath}'"); + if (!PSScriptFileInfo.TryTestPSScriptFileInfo(ps1FilePath, out PSScriptFileInfo parsedScript, out ErrorRecord[] errors, out string[] verboseMsgs)) + { + errRecord = new ErrorRecord( + new InvalidDataException($"PSScriptFile could not be read properly"), + "GetMetadataFromNupkgFailure", + ErrorCategory.ParserError, + this); + + return pkgMetadata; + } + + pkgMetadata = parsedScript.ToHashtable(); + } + else if (File.Exists(nuspecFilePath)) + { + _cmdletPassedIn.WriteDebug($"Attempting to read nuspec file '{nuspecFilePath}'"); + pkgMetadata = Utils.GetMetadataFromNuspec(nuspecFilePath, _cmdletPassedIn, out errRecord); + if (errRecord != null) + { + return pkgMetadata; + } + } + else + { + errRecord = new ErrorRecord( + new InvalidDataException($".nupkg package must contain either .psd1, .ps1, or .nuspec file and none were found"), + "GetMetadataFromNupkgFailure", + ErrorCategory.InvalidData, + this); + + return pkgMetadata; + } + } + catch (Exception e) + { + errRecord = new ErrorRecord( + new InvalidOperationException($"Temporary folder for installation could not be created or set due to: {e.Message}"), + "GetMetadataFromNupkgFailure", + ErrorCategory.InvalidOperation, + this); + } + finally + { + if (Directory.Exists(extractPath)) + { + Utils.DeleteDirectory(extractPath); + } + } + + return pkgMetadata; + } + #endregion } } diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index 97e9f80b7..e9f2693e2 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -862,7 +862,7 @@ private static PSRepositoryInfo.APIVersion GetRepoAPIVersion(Uri repoUri) // repositories with Uri Scheme "temp" may have PSPath Uri's like: "Temp:\repo" and we should consider them as local repositories. return PSRepositoryInfo.APIVersion.Local; } - else if (repoUri.AbsoluteUri.EndsWith(".azurecr.io") || repoUri.AbsoluteUri.EndsWith(".azurecr.io/")) + else if (repoUri.AbsoluteUri.EndsWith(".azurecr.io") || repoUri.AbsoluteUri.EndsWith(".azurecr.io/") || repoUri.AbsoluteUri.Contains("mcr.microsoft.com")) { return PSRepositoryInfo.APIVersion.ContainerRegistry; } @@ -876,7 +876,7 @@ private static RepositoryProviderType GetRepositoryProviderType(Uri repoUri) { string absoluteUri = repoUri.AbsoluteUri; // We want to use contains instead of EndsWith to accomodate for trailing '/' - if (absoluteUri.Contains("azurecr.io")){ + if (absoluteUri.Contains("azurecr.io") || absoluteUri.Contains("mcr.microsoft.com")){ return RepositoryProviderType.ACR; } // TODO: add a regex for this match diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index ce5581ad7..26d481fce 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -161,7 +161,6 @@ public string TemporaryPath /// /// Check validation for signed and catalog files - /// [Parameter] public SwitchParameter AuthenticodeCheck { get; set; } @@ -169,6 +168,7 @@ public string TemporaryPath /// /// Suppresses progress information. /// + [Parameter] public SwitchParameter Quiet { get; set; } /// diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 769329d84..d51ba0fdb 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -23,6 +23,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using System.Xml; namespace Microsoft.PowerShell.PSResourceGet.UtilClasses { @@ -1172,11 +1173,12 @@ internal static HashSet GetInstalledPackages(List pathsToSearch, return pkgsInstalledOnMachine; } - internal static void GetMetadataFilesFromPath(string dirPath, string packageName, out string psd1FilePath, out string ps1FilePath, out string nuspecFilePath) + internal static void GetMetadataFilesFromPath(string dirPath, string packageName, out string psd1FilePath, out string ps1FilePath, out string nuspecFilePath, out string properCasingPkgName) { psd1FilePath = String.Empty; ps1FilePath = String.Empty; nuspecFilePath = String.Empty; + properCasingPkgName = packageName; var discoveredFiles = Directory.GetFiles(dirPath, "*.*", SearchOption.AllDirectories); string pkgNamePattern = $"{packageName}*"; @@ -1185,16 +1187,29 @@ internal static void GetMetadataFilesFromPath(string dirPath, string packageName { if (rgx.IsMatch(file)) { - if (file.EndsWith("psd1")) + string fileName = Path.GetFileName(file); + if (fileName.EndsWith("psd1")) { + if (string.Compare($"{packageName}.psd1", fileName, StringComparison.OrdinalIgnoreCase) == 0) + { + properCasingPkgName = Path.GetFileNameWithoutExtension(file); + } psd1FilePath = file; } else if (file.EndsWith("nuspec")) { + if (string.Compare($"{packageName}.nuspec", fileName, StringComparison.OrdinalIgnoreCase) == 0) + { + properCasingPkgName = Path.GetFileNameWithoutExtension(file); + } nuspecFilePath = file; } else if (file.EndsWith("ps1")) { + if (string.Compare($"{packageName}.ps1", fileName, StringComparison.OrdinalIgnoreCase) == 0) + { + properCasingPkgName = Path.GetFileNameWithoutExtension(file); + } ps1FilePath = file; } } @@ -1569,6 +1584,11 @@ public static void DeleteDirectoryWithRestore(string dirPath) /// public static void DeleteDirectory(string dirPath) { + if (!Directory.Exists(dirPath)) + { + throw new Exception($"Path '{dirPath}' that was attempting to be deleted does not exist."); + } + // Remove read only file attributes first foreach (var dirFilePath in Directory.GetFiles(dirPath,"*",SearchOption.AllDirectories)) { @@ -1816,6 +1836,73 @@ public static void CreateFile(string filePath) #endregion + #region Nuspec file parsing methods + + public static Hashtable GetMetadataFromNuspec(string nuspecFilePath, PSCmdlet cmdletPassedIn, out ErrorRecord errorRecord) + { + Hashtable nuspecHashtable = new Hashtable(StringComparer.InvariantCultureIgnoreCase); + + XmlDocument nuspecXmlDocument = LoadXmlDocument(nuspecFilePath, cmdletPassedIn, out errorRecord); + if (errorRecord != null) + { + return nuspecHashtable; + } + + try + { + XmlNodeList elemList = nuspecXmlDocument.GetElementsByTagName("metadata"); + for(int i = 0; i < elemList.Count; i++) + { + XmlNode metadataInnerXml = elemList[i]; + + for(int j= 0; j + /// Method that loads file content into XMLDocument. Used when reading .nuspec file. + /// + public static XmlDocument LoadXmlDocument(string filePath, PSCmdlet cmdletPassedIn, out ErrorRecord errRecord) + { + errRecord = null; + XmlDocument doc = new XmlDocument(); + doc.PreserveWhitespace = true; + try { doc.Load(filePath); } + catch (Exception e) + { + errRecord = new ErrorRecord( + exception: e, + "LoadXmlDocumentFailure", + ErrorCategory.ReadError, + cmdletPassedIn); + } + + return doc; + } + + #endregion + } #endregion diff --git a/src/code/V2ServerAPICalls.cs b/src/code/V2ServerAPICalls.cs index 59b1a3ab4..38b5640de 100644 --- a/src/code/V2ServerAPICalls.cs +++ b/src/code/V2ServerAPICalls.cs @@ -355,7 +355,7 @@ public override FindResults FindName(string packageName, bool includePrerelease, filterBuilder.AddCriterion($"Id eq '{packageName}'"); } - filterBuilder.AddCriterion(includePrerelease ? "IsAbsoluteLatestVersion" : "IsLatestVersion"); + filterBuilder.AddCriterion(includePrerelease ? "IsAbsoluteLatestVersion eq true" : "IsLatestVersion eq true"); if (type != ResourceType.None) { filterBuilder.AddCriterion(GetTypeFilterForRequest(type)); } @@ -424,7 +424,7 @@ public override FindResults FindNameWithTag(string packageName, string[] tags, b filterBuilder.AddCriterion($"Id eq '{packageName}'"); } - filterBuilder.AddCriterion(includePrerelease ? "IsAbsoluteLatestVersion" : "IsLatestVersion"); + filterBuilder.AddCriterion(includePrerelease ? "IsAbsoluteLatestVersion eq true" : "IsLatestVersion eq true"); if (type != ResourceType.None) { filterBuilder.AddCriterion(GetTypeFilterForRequest(type)); } @@ -915,14 +915,26 @@ private string FindAllFromTypeEndPoint(bool includePrerelease, bool isSearchingM // JFrog/Artifactory requires an empty search term to enumerate all packages in the feed if (_isJFrogRepo) { queryBuilder.SearchTerm = "''"; - } - if (includePrerelease) { - queryBuilder.AdditionalParameters["includePrerelease"] = "true"; - filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); - } else { - filterBuilder.AddCriterion("IsLatestVersion"); + if (includePrerelease) { + queryBuilder.AdditionalParameters["includePrerelease"] = "true"; + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsAbsoluteLatestVersion correctly + filterBuilder.AddCriterion("IsAbsoluteLatestVersion eq true"); + } else { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsLatestVersion correctly + filterBuilder.AddCriterion("IsLatestVersion eq true"); + } } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + if (includePrerelease) { + queryBuilder.AdditionalParameters["includePrerelease"] = "true"; + filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + } else { + filterBuilder.AddCriterion("IsLatestVersion"); + } + } + var requestUrlV2 = $"{Repository.Uri}{typeEndpoint}/Search()?{queryBuilder.BuildQueryString()}"; return HttpRequestCall(requestUrlV2, out errRecord); } @@ -952,16 +964,24 @@ private string FindTagFromEndpoint(string[] tags, bool includePrerelease, bool i queryBuilder.AdditionalParameters["$orderby"] = "Id desc"; } - // JFrog/Artifactory requires an empty search term to enumerate all packages in the feed - if (_isJFrogRepo) { - queryBuilder.SearchTerm = "''"; - } - if (includePrerelease) { queryBuilder.AdditionalParameters["includePrerelease"] = "true"; - filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + if (_isJFrogRepo) { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsAbsoluteLatestVersion correctly + filterBuilder.AddCriterion("IsAbsoluteLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + } } else { - filterBuilder.AddCriterion("IsLatestVersion"); + if (_isJFrogRepo) { + filterBuilder.AddCriterion("IsLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsLatestVersion"); + } } filterBuilder.AddCriterion($"substringof('PS{(isSearchingModule ? "Module" : "Script")}', Tags) eq true"); @@ -996,12 +1016,25 @@ private string FindCommandOrDscResource(string[] tags, bool includePrerelease, b if (includePrerelease) { queryBuilder.AdditionalParameters["includePrerelease"] = "true"; - filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + if (_isJFrogRepo) { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsAbsoluteLatestVersion correctly + filterBuilder.AddCriterion("IsAbsoluteLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + } } else { - filterBuilder.AddCriterion("IsLatestVersion"); + if (_isJFrogRepo) { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsLatestVersion correctly + filterBuilder.AddCriterion("IsLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsLatestVersion"); + } } - // can only find from Modules endpoint var tagPrefix = isSearchingForCommands ? "PSCommand_" : "PSDscResource_"; @@ -1038,12 +1071,25 @@ private string FindNameGlobbing(string packageName, ResourceType type, bool incl if (includePrerelease) { queryBuilder.AdditionalParameters["includePrerelease"] = "true"; - filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + if (_isJFrogRepo) { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsAbsoluteLatestVersion correctly + filterBuilder.AddCriterion("IsAbsoluteLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + } } else { - filterBuilder.AddCriterion("IsLatestVersion"); + if (_isJFrogRepo) { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsLatestVersion correctly + filterBuilder.AddCriterion("IsLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsLatestVersion"); + } } - var names = packageName.Split(new char[] {'*'}, StringSplitOptions.RemoveEmptyEntries); if (names.Length == 0) @@ -1131,14 +1177,28 @@ private string FindNameGlobbingWithTag(string packageName, string[] tags, Resour queryBuilder.AdditionalParameters["$orderby"] = "Id desc"; } + // JFrog/Artifactory requires an empty search term to enumerate all packages in the feed if (includePrerelease) { queryBuilder.AdditionalParameters["includePrerelease"] = "true"; - filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + if (_isJFrogRepo) { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsAbsoluteLatestVersion correctly + filterBuilder.AddCriterion("IsAbsoluteLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsAbsoluteLatestVersion"); + } } else { - filterBuilder.AddCriterion("IsLatestVersion"); + if (_isJFrogRepo) { + // note: we add 'eq true' because some PMPs (currently we know of JFrog, but others may do this too) will proxy the query unedited to the upstream remote and if that's PSGallery, it doesn't handle IsLatestVersion correctly + filterBuilder.AddCriterion("IsLatestVersion eq true"); + } + else { + // For ADO, 'IsLatestVersion eq true' and 'IsAbsoluteLatestVersion eq true' in the filter create a bad request error, so we use 'IsLatestVersion' or 'IsAbsoluteLatestVersion' only + filterBuilder.AddCriterion("IsLatestVersion"); + } } - var names = packageName.Split(new char[] {'*'}, StringSplitOptions.RemoveEmptyEntries); if (!_isPSGalleryRepo) diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index 8f5297ecc..c184426a8 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -1622,9 +1622,11 @@ private HttpContent HttpRequestCallForContent(string requestUrlV3, out ErrorReco "HttpRequestCallForContentFailure", ErrorCategory.InvalidResult, this); + + return null; } - if (string.IsNullOrEmpty(content.ToString())) + if (string.IsNullOrEmpty(content?.ToString())) { _cmdletPassedIn.WriteDebug("Response is empty"); } diff --git a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 index 5de4d7c46..b7ffdfb8e 100644 --- a/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceContainerRegistryServer.Tests.ps1 @@ -227,4 +227,32 @@ Describe 'Test HTTP Find-PSResource for ACR Server Protocol' -tags 'CI' { $res.Version | Should -Be "1.0.0" $res.Type.ToString() | Should -Be "Script" } + + It "Should find resource with dependency, given Name and Version" { + $res = Find-PSResource -Name "Az.Storage" -Version "8.0.0" -Repository $ACRRepoName + $res.Dependencies.Length | Should -Be 1 + $res.Dependencies[0].Name | Should -Be "Az.Accounts" + } +} + +Describe 'Test Find-PSResource for MAR Repository' -tags 'CI' { + BeforeAll { + Register-PSResourceRepository -Name "MAR" -Uri "https://mcr.microsoft.com" -ApiVersion "ContainerRegistry" + } + + AfterAll { + Unregister-PSResourceRepository -Name "MAR" + } + + It "Should find resource given specific Name, Version null" { + $res = Find-PSResource -Name "Az.Accounts" -Repository "MAR" + $res.Name | Should -Be "Az.Accounts" + $res.Version | Should -Be "4.0.0" + } + + It "Should find resource and its dependency given specific Name and Version" { + $res = Find-PSResource -Name "Az.Storage" -Version "8.0.0" -Repository "MAR" + $res.Dependencies.Length | Should -Be 1 + $res.Dependencies[0].Name | Should -Be "Az.Accounts" + } } diff --git a/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 index b6e43716a..b89a245ee 100644 --- a/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 @@ -14,12 +14,15 @@ Describe 'Test Find-PSResource for local repositories' -tags 'CI' { $localUNCRepo = 'psgettestlocal3' $testModuleName = "test_local_mod" $testModuleName2 = "test_local_mod2" + $testModuleName3 = "Test_Local_Mod3" $similarTestModuleName = "test_local_mod.similar" $commandName = "cmd1" $dscResourceName = "dsc1" $prereleaseLabel = "" + $localNupkgRepo = "localNupkgRepo" Get-NewPSResourceRepositoryFile Register-LocalRepos + Register-LocalTestNupkgsRepo $localRepoUriAddress = Join-Path -Path $TestDrive -ChildPath "testdir" $tagsEscaped = @("'Test'", "'Tag2'", "'PSCommand_$cmdName'", "'PSDscResource_$dscName'") @@ -33,6 +36,8 @@ Describe 'Test Find-PSResource for local repositories' -tags 'CI' { New-TestModule -moduleName $testModuleName2 -repoName $localRepo -packageVersion "5.0.0" -prereleaseLabel "" -tags $tagsEscaped New-TestModule -moduleName $testModuleName2 -repoName $localRepo -packageVersion "5.2.5" -prereleaseLabel $prereleaseLabel -tags $tagsEscaped + New-TestModule -moduleName $testModuleName3 -repoName $localRepo -packageVersion "1.0.0" -prereleaseLabel "" -tags @() + New-TestModule -moduleName $similarTestModuleName -repoName $localRepo -packageVersion "4.0.0" -prereleaseLabel "" -tags $tagsEscaped New-TestModule -moduleName $similarTestModuleName -repoName $localRepo -packageVersion "5.0.0" -prereleaseLabel "" -tags $tagsEscaped } @@ -48,6 +53,20 @@ Describe 'Test Find-PSResource for local repositories' -tags 'CI' { $res.Version | Should -Be "5.0.0" } + It "find resource given specific Name with incorrect casing (should return correct casing)" { + # FindName() + $res = Find-PSResource -Name "test_local_mod3" -Repository $localRepo + $res.Name | Should -Be $testModuleName3 + $res.Version | Should -Be "1.0.0" + } + + It "find resource given specific Name with incorrect casing and Version (should return correct casing)" { + # FindVersion() + $res = Find-PSResource -Name "test_local_mod3" -Version "1.0.0" -Repository $localRepo + $res.Name | Should -Be $testModuleName3 + $res.Version | Should -Be "1.0.0" + } + It "find resource given specific Name, Version null (module) from a UNC-based local repository" { # FindName() $res = Find-PSResource -Name $testModuleName -Repository $localUNCRepo @@ -309,4 +328,14 @@ Describe 'Test Find-PSResource for local repositories' -tags 'CI' { $err.Count | Should -Not -Be 0 $err[0].FullyQualifiedErrorId | Should -BeExactly "FindTagsPackageNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" } + + It "find package where prerelease label includes digits and period (i.e prerelease label is not just words)" { + $nupkgName = "WebView2.Avalonia" + $nupkgVersion = "1.0.1518.46" + $prereleaseLabel = "preview.230207.17" + $res = Find-PSResource -Name $nupkgName -Prerelease -Repository $localNupkgRepo + $res.Name | Should -Be $nupkgName + $res.Version | Should -Be $nupkgVersion + $res.Prerelease | Should -Be $prereleaseLabel + } } diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 835043da7..2ade007f2 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -137,7 +137,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { It "Install resource with a dependency (should install both parent and dependency)" { Install-PSResource -Name $testModuleParentName -Repository $ACRRepoName -TrustRepository - + $parentPkg = Get-InstalledPSResource $testModuleParentName $parentPkg.Name | Should -Be $testModuleParentName $parentPkg.Version | Should -Be "1.0.0" @@ -307,3 +307,28 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidatio Set-PSResourceRepository PoshTestGallery -Trusted } } + +Describe 'Test Install-PSResource for MAR Repository' -tags 'CI' { + BeforeAll { + [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", "azure-powershell/"); + Register-PSResourceRepository -Name "MAR" -Uri "https://mcr.microsoft.com" -ApiVersion "ContainerRegistry" + } + + AfterAll { + [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", $null); + Unregister-PSResourceRepository -Name "MAR" + } + + It "Should find resource given specific Name, Version null" { + try { + $pkg = Install-PSResource -Name "Az.Accounts" -Repository "MAR" -PassThru -TrustRepository -Reinstall + $pkg.Name | Should -Be "Az.Accounts" + $pkg.Version | Should -Be "3.0.4" + } + finally { + if ($pkg) { + Uninstall-PSResource -Name "Az.Accounts" -Version "3.0.4" + } + } + } +} diff --git a/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 index 437fcd1ad..f5ec1d02b 100644 --- a/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 @@ -25,7 +25,7 @@ Describe 'Test Install-PSResource for local repositories' -tags 'CI' { Register-LocalRepos Register-LocalTestNupkgsRepo - $prereleaseLabel = "alpha001" + $prereleaseLabel = "Alpha001" $tags = @() New-TestModule -moduleName $testModuleName -repoName $localRepo -packageVersion "1.0.0" -prereleaseLabel "" -tags $tags @@ -131,12 +131,12 @@ Describe 'Test Install-PSResource for local repositories' -tags 'CI' { $pkg.Version | Should -Be "3.0.0" } - It "Install resource with latest (including prerelease) version given Prerelease parameter" { + It "Install resource with latest (including prerelease) version given Prerelease parameter (prerelease casing should be correct)" { Install-PSResource -Name $testModuleName -Prerelease -Repository $localRepo -TrustRepository $pkg = Get-InstalledPSResource $testModuleName $pkg.Name | Should -Be $testModuleName $pkg.Version | Should -Be "5.2.5" - $pkg.Prerelease | Should -Be "alpha001" + $pkg.Prerelease | Should -Be "Alpha001" } It "Install resource with cmdlet names from a module already installed with -NoClobber (should not clobber)" { @@ -205,7 +205,7 @@ Describe 'Test Install-PSResource for local repositories' -tags 'CI' { # Windows only It "Install resource under AllUsers scope - Windows only" -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { - Install-PSResource -Name $testModuleName -Repository $localRepo -TrustRepository -Scope AllUsers -Verbose + Install-PSResource -Name $testModuleName -Repository $localRepo -TrustRepository -Scope AllUsers $pkg = Get-InstalledPSResource $testModuleName -Scope AllUsers $pkg.Name | Should -Be $testModuleName $pkg.InstalledLocation.ToString().Contains("Program Files") | Should -Be $true @@ -290,10 +290,8 @@ Describe 'Test Install-PSResource for local repositories' -tags 'CI' { $nupkgName = "Microsoft.Web.Webview2" $nupkgVersion = "1.0.2792.45" $repoPath = Get-PSResourceRepository $localNupkgRepo - Write-Verbose -Verbose "repoPath $($repoPath.Uri)" $searchPkg = Find-PSResource -Name $nupkgName -Version $nupkgVersion -Repository $localNupkgRepo - Write-Verbose -Verbose "search name: $($searchPkg.Name)" - Install-PSResource -Name $nupkgName -Version $nupkgVersion -Repository $localNupkgRepo -TrustRepository -Verbose + Install-PSResource -Name $nupkgName -Version $nupkgVersion -Repository $localNupkgRepo -TrustRepository $pkg = Get-InstalledPSResource $nupkgName $pkg.Name | Should -Be $nupkgName $pkg.Version | Should -Be $nupkgVersion diff --git a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 index 1b2a70d84..84e941dfa 100644 --- a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 @@ -270,16 +270,20 @@ Describe "Test Publish-PSResource" -tags 'CI' { $dependencyVersion = "2.0.0" New-ModuleManifest -Path (Join-Path -Path $script:DependencyModuleBase -ChildPath "$script:DependencyModuleName.psd1") -ModuleVersion $dependencyVersion -Description "$script:DependencyModuleName module" - Publish-PSResource -Path $script:DependencyModuleBase + Publish-PSResource -Path $script:DependencyModuleBase -Repository $testRepository2 + $pkg1 = Find-PSResource $script:DependencyModuleName -Repository $testRepository2 + $pkg1 | Should -Not -BeNullOrEmpty + $pkg1.Version | Should -Be $dependencyVersion # Create module to test $version = "1.0.0" New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" -RequiredModules @(@{ModuleName = 'PackageManagement'; ModuleVersion = '2.0.0' }) - Publish-PSResource -Path $script:PublishModuleBase + Publish-PSResource -Path $script:PublishModuleBase -Repository $testRepository2 - $nupkg = Get-ChildItem $script:repositoryPath | select-object -Last 1 - $nupkg.Name | Should -Be "$script:PublishModuleName.$version.nupkg" + $pkg2 = Find-PSResource $script:DependencyModuleName -Repository $testRepository2 + $pkg2 | Should -Not -BeNullOrEmpty + $pkg2.Version | Should -Be $dependencyVersion } It "Publish a module with a dependency that is not published, should throw" { @@ -685,7 +689,7 @@ Describe "Test Publish-PSResource" -tags 'CI' { $expectedPath = Join-Path -Path $script:repositoryPath2 -ChildPath "$ParentModuleName.$ParentVersion.nupkg" (Get-ChildItem $script:repositoryPath2).FullName | Should -Contain $expectedPath } - +<# It "Publish a module with required modules (both in string format and hashtable format)" { # look at functions in test utils for creating a module with prerelease $ModuleName = "ParentModule" @@ -720,4 +724,5 @@ Describe "Test Publish-PSResource" -tags 'CI' { $expectedPath = Join-Path -Path $script:repositoryPath2 -ChildPath "$ModuleName.$ModuleVersion.nupkg" (Get-ChildItem $script:repositoryPath2).FullName | Should -Contain $expectedPath } +#> } diff --git a/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 index 6c4150dac..af57385a1 100644 --- a/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 @@ -97,6 +97,9 @@ Describe "Test Publish-PSResource" -tags 'CI' { # Path to specifically to that invalid test scripts folder $script:testScriptsFolderPath = Join-Path $script:testFilesFolderPath -ChildPath "testScripts" + + # Path to specifically to that invalid test nupkgs folder + $script:testNupkgsFolderPath = Join-Path $script:testFilesFolderPath -ChildPath "testNupkgs" } AfterEach { if(!(Test-Path $script:PublishModuleBase)) @@ -511,4 +514,61 @@ Describe "Test Publish-PSResource" -tags 'CI' { $results[0].Name | Should -Be $script:PublishModuleName $results[0].Version | Should -Be $version } + + It "Publish a package given NupkgPath to a package with .psd1" { + $packageName = "temp-testmodule-nupkgpath" + $version = "1.0.0.0" + $nupkgPath = Join-Path -Path $script:testNupkgsFolderPath -ChildPath "$packageName.1.0.0.nupkg" + Publish-PSResource -NupkgPath $nupkgPath -Repository $ACRRepoName + + $results = Find-PSResource -Name $packageName -Repository $ACRRepoName + $results | Should -Not -BeNullOrEmpty + $results[0].Name | Should -Be $packageName + $results[0].Version | Should -Be $version + } + + It "Publish a package given NupkgPath to a package with .ps1" { + $packageName = "temp-testscript-nupkgpath" + $version = "1.0.0.0" + $nupkgPath = Join-Path -Path $script:testNupkgsFolderPath -ChildPath "$packageName.1.0.0.nupkg" + Publish-PSResource -NupkgPath $nupkgPath -Repository $ACRRepoName + + $results = Find-PSResource -Name $packageName -Repository $ACRRepoName + $results | Should -Not -BeNullOrEmpty + $results[0].Name | Should -Be $packageName + $results[0].Version | Should -Be $version + } + + It "Publish a package given NupkgPath to a package with .nuspec" { + $packageName = "temp-testnupkg-nupkgpath" + $version = "1.0.0" + $nupkgPath = Join-Path -Path $script:testNupkgsFolderPath -ChildPath "$packageName.1.0.0.nupkg" + Publish-PSResource -NupkgPath $nupkgPath -Repository $ACRRepoName + + $results = Find-PSResource -Name $packageName -Repository $ACRRepoName + $results | Should -Not -BeNullOrEmpty + $results[0].Name | Should -Be $packageName + $results[0].Version | Should -Be $version + } +} + +Describe 'Test Publish-PSResource for MAR Repository' -tags 'CI' { + BeforeAll { + [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", "azure-powershell/"); + Register-PSResourceRepository -Name "MAR" -Uri "https://mcr.microsoft.com" -ApiVersion "ContainerRegistry" + } + + AfterAll { + [Microsoft.PowerShell.PSResourceGet.UtilClasses.InternalHooks]::SetTestHook("MARPrefix", $null); + Unregister-PSResourceRepository -Name "MAR" + } + + It "Should find resource given specific Name, Version null" { + $fileName = "NonExistent.psd1" + $modulePath = New-Item -Path "$TestDrive\NonExistent" -ItemType Directory -Force + $psd1Path = Join-Path -Path $modulePath -ChildPath $fileName + New-ModuleManifest -Path $psd1Path -ModuleVersion "1.0.0" -Description "NonExistent module" + + { Publish-PSResource -Path $modulePath -Repository "MAR" -ErrorAction Stop } | Should -Throw -ErrorId "MARRepositoryPublishError,Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource" + } } diff --git a/test/SavePSResourceTests/SavePSResourceLocalTests.ps1 b/test/SavePSResourceTests/SavePSResourceLocalTests.ps1 index ba94dc71e..36eb5330c 100644 --- a/test/SavePSResourceTests/SavePSResourceLocalTests.ps1 +++ b/test/SavePSResourceTests/SavePSResourceLocalTests.ps1 @@ -198,4 +198,10 @@ Describe 'Test Save-PSResource for local repositories' -tags 'CI' { $err.Count | Should -Not -BeNullOrEmpty $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource" } + + It "Save module using -Quiet" { + $res = Save-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -Path $SaveDir -PassThru -TrustRepository -Quiet + $res.Name | Should -Be $moduleName + $res.Version | Should -Be "1.0.0" + } } diff --git a/test/testFiles/testNupkgs/temp-testmodule-nupkgpath.1.0.0.nupkg b/test/testFiles/testNupkgs/temp-testmodule-nupkgpath.1.0.0.nupkg new file mode 100644 index 000000000..a0f7d11d8 Binary files /dev/null and b/test/testFiles/testNupkgs/temp-testmodule-nupkgpath.1.0.0.nupkg differ diff --git a/test/testFiles/testNupkgs/temp-testnupkg-nupkgpath.1.0.0.nupkg b/test/testFiles/testNupkgs/temp-testnupkg-nupkgpath.1.0.0.nupkg new file mode 100644 index 000000000..370ba0068 Binary files /dev/null and b/test/testFiles/testNupkgs/temp-testnupkg-nupkgpath.1.0.0.nupkg differ diff --git a/test/testFiles/testNupkgs/temp-testscript-nupkgpath.1.0.0.nupkg b/test/testFiles/testNupkgs/temp-testscript-nupkgpath.1.0.0.nupkg new file mode 100644 index 000000000..e3adbf814 Binary files /dev/null and b/test/testFiles/testNupkgs/temp-testscript-nupkgpath.1.0.0.nupkg differ diff --git a/test/testFiles/testNupkgs/webview2.avalonia.1.0.1518.46-preview.230207.17.nupkg b/test/testFiles/testNupkgs/webview2.avalonia.1.0.1518.46-preview.230207.17.nupkg new file mode 100644 index 000000000..e47c80773 Binary files /dev/null and b/test/testFiles/testNupkgs/webview2.avalonia.1.0.1518.46-preview.230207.17.nupkg differ