diff --git a/.gitignore b/.gitignore index df39f04f3..e035e30c7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ srcOld/code/obj out test/**/obj test/**/bin +.vs +.vscode +src/code/.vs +test/testFiles/testScripts/test.ps1 \ No newline at end of file diff --git a/src/Microsoft.PowerShell.PSResourceGet.psd1 b/src/Microsoft.PowerShell.PSResourceGet.psd1 index b5abe9ae6..2b631ee21 100644 --- a/src/Microsoft.PowerShell.PSResourceGet.psd1 +++ b/src/Microsoft.PowerShell.PSResourceGet.psd1 @@ -16,6 +16,7 @@ CLRVersion = '4.0.0' FormatsToProcess = 'PSGet.Format.ps1xml' CmdletsToExport = @( + 'Compress-PSResource', 'Find-PSResource', 'Get-InstalledPSResource', 'Get-PSResourceRepository', diff --git a/src/code/.vs/Microsoft.PowerShell.PSResourceGet/v17/.suo b/src/code/.vs/Microsoft.PowerShell.PSResourceGet/v17/.suo index 05b0bcf91..12eaeb3a3 100644 Binary files a/src/code/.vs/Microsoft.PowerShell.PSResourceGet/v17/.suo and b/src/code/.vs/Microsoft.PowerShell.PSResourceGet/v17/.suo differ diff --git a/src/code/CompressPSResource.cs b/src/code/CompressPSResource.cs new file mode 100644 index 000000000..ba79425df --- /dev/null +++ b/src/code/CompressPSResource.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PSResourceGet.UtilClasses; +using System.IO; +using System.Linq; +using System.Management.Automation; + +namespace Microsoft.PowerShell.PSResourceGet.Cmdlets +{ + /// + /// Compresses a module, script, or nupkg to a designated repository. + /// + [Cmdlet(VerbsData.Compress, + "PSResource", + SupportsShouldProcess = true)] + [Alias("cmres")] + public sealed class CompressPSResource : PSCmdlet + { + #region Parameters + + /// + /// Specifies the path to the resource that you want to compress. This parameter accepts the path to the folder that contains the resource. + /// Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory (.). + /// + [Parameter(Mandatory = true, Position = 0, HelpMessage = "Path to the resource to be compressed.")] + [ValidateNotNullOrEmpty] + public string Path { get; set; } + + /// + /// Specifies the path where the compressed resource (as a .nupkg file) should be saved. + /// This parameter allows you to save the package to a specified location on the local file system. + /// + [Parameter(Mandatory = true, Position = 1, HelpMessage = "Path to save the compressed resource.")] + [ValidateNotNullOrEmpty] + public string DestinationPath { get; set; } + + /// + /// Bypasses validating a resource module manifest before publishing. + /// + [Parameter] + public SwitchParameter SkipModuleManifestValidate { get; set; } + + #endregion + + #region Members + + private PublishHelper _publishHelper; + + #endregion + + #region Method Overrides + + protected override void BeginProcessing() + { + // Create a respository store (the PSResourceRepository.xml file) if it does not already exist + // This is to create a better experience for those who have just installed v3 and want to get up and running quickly + RepositorySettings.CheckRepositoryStore(); + + _publishHelper = new PublishHelper( + this, + Path, + DestinationPath, + SkipModuleManifestValidate); + + _publishHelper.CheckAllParameterPaths(); + } + + protected override void EndProcessing() + { + _publishHelper.PackResource(); + } + + #endregion + + } +} diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 9e2fc6bc2..0e14f6ce9 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -21,7 +21,8 @@ public enum ResourceType { None, Module, - Script + Script, + Nupkg } public enum VersionType diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs new file mode 100644 index 000000000..f8c762eab --- /dev/null +++ b/src/code/PublishHelper.cs @@ -0,0 +1,1211 @@ +using Microsoft.PowerShell.PSResourceGet.UtilClasses; +using NuGet.Commands; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging; +using NuGet.Versioning; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Xml; + +namespace Microsoft.PowerShell.PSResourceGet.Cmdlets +{ + internal class PublishHelper + { + #region Enums + internal enum CallerCmdlet + { + PublishPSResource, + CompressPSResource + } + + #endregion + + #region Members + + private readonly CallerCmdlet _callerCmdlet; + private readonly PSCmdlet _cmdletPassedIn; + private readonly string _cmdOperation; + private readonly string Path; + private string DestinationPath; + private string resolvedPath; + private CancellationToken _cancellationToken; + private NuGetVersion _pkgVersion; + private string _pkgName; + private static char[] _PathSeparators = new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar }; + public const string PSDataFileExt = ".psd1"; + public const string PSScriptFileExt = ".ps1"; + public const string NupkgFileExt = ".nupkg"; + private const string PSScriptInfoCommentString = "<#PSScriptInfo"; + private string pathToScriptFileToPublish = string.Empty; + private string pathToModuleManifestToPublish = string.Empty; + private string pathToModuleDirToPublish = string.Empty; + private string pathToNupkgToPublish = string.Empty; + private ResourceType resourceType = ResourceType.None; + private NetworkCredential _networkCredential; + string userAgentString = UserAgentInfo.UserAgentString(); + private bool _isNupkgPathSpecified = false; + private Hashtable dependencies; + private Hashtable parsedMetadata; + private PSCredential Credential; + private string outputNupkgDir; + private string ApiKey; + private bool SkipModuleManifestValidate = false; + private string outputDir = string.Empty; + internal bool ScriptError = false; + internal bool ShouldProcess = true; + + #endregion + + #region Constructors + + internal PublishHelper(PSCmdlet cmdlet, string path, string destinationPath, bool skipModuleManifestValidate) + { + _callerCmdlet = CallerCmdlet.CompressPSResource; + _cmdOperation = "Compress"; + _cmdletPassedIn = cmdlet; + Path = path; + DestinationPath = destinationPath; + SkipModuleManifestValidate = skipModuleManifestValidate; + outputDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); + outputNupkgDir = destinationPath; + } + + internal PublishHelper(PSCmdlet cmdlet, + PSCredential credential, + string apiKey, + string path, + string destinationPath, + bool skipModuleManifestValidate, + CancellationToken cancellationToken, + bool isNupkgPathSpecified) + { + _callerCmdlet = CallerCmdlet.PublishPSResource; + _cmdOperation = "Publish"; + _cmdletPassedIn = cmdlet; + Credential = credential; + ApiKey = apiKey; + Path = path; + DestinationPath = destinationPath; + SkipModuleManifestValidate = skipModuleManifestValidate; + _cancellationToken = cancellationToken; + _isNupkgPathSpecified = isNupkgPathSpecified; + outputDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); + outputNupkgDir = System.IO.Path.Combine(outputDir, "nupkg"); + } + + #endregion + + #region Internal Methods + + internal void PackResource() + { + // Returns the name of the file or the name of the directory, depending on path + if (!_cmdletPassedIn.ShouldProcess(string.Format("'{0}' from the machine", resolvedPath))) + { + _cmdletPassedIn.WriteVerbose("ShouldProcess is set to false."); + ShouldProcess = false; + return; + } + + parsedMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase); + if (resourceType == ResourceType.Script) + { + if (!PSScriptFileInfo.TryTestPSScriptFileInfo( + scriptFileInfoPath: pathToScriptFileToPublish, + parsedScript: out PSScriptFileInfo scriptToPublish, + out ErrorRecord[] errors, + out string[] _ + )) + { + foreach (ErrorRecord error in errors) + { + _cmdletPassedIn.WriteError(error); + } + + ScriptError = true; + + return; + } + + parsedMetadata = scriptToPublish.ToHashtable(); + + _pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToScriptFileToPublish); + } + else + { + if (!string.IsNullOrEmpty(pathToModuleManifestToPublish)) + { + _pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToModuleManifestToPublish); + } + else + { + // Search for module manifest + foreach (FileInfo file in new DirectoryInfo(pathToModuleDirToPublish).EnumerateFiles()) + { + if (file.Name.EndsWith(PSDataFileExt, StringComparison.OrdinalIgnoreCase)) + { + pathToModuleManifestToPublish = file.FullName; + _pkgName = System.IO.Path.GetFileNameWithoutExtension(file.Name); + + break; + } + } + } + + // Validate that there's a module manifest + if (!File.Exists(pathToModuleManifestToPublish)) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"No file with a .psd1 extension was found in '{pathToModuleManifestToPublish}'. Please specify a path to a valid module manifest."), + "moduleManifestNotFound", + ErrorCategory.ObjectNotFound, + this)); + + return; + } + + // The Test-ModuleManifest currently cannot process UNC paths. Disabling verification for now. + if ((new Uri(pathToModuleManifestToPublish)).IsUnc) + SkipModuleManifestValidate = true; + + // Validate that the module manifest has correct data + if (!SkipModuleManifestValidate && + !Utils.ValidateModuleManifest(pathToModuleManifestToPublish, out string errorMsg)) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSInvalidOperationException(errorMsg), + "InvalidModuleManifest", + ErrorCategory.InvalidOperation, + this)); + } + + if (!Utils.TryReadManifestFile( + manifestFilePath: pathToModuleManifestToPublish, + manifestInfo: out parsedMetadata, + error: out Exception manifestReadError)) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + manifestReadError, + "ManifestFileReadParseForContainerRegistryPublishError", + ErrorCategory.ReadError, + this)); + + return; + } + + } + + // Create a temp folder to push the nupkg to and delete it later + try + { + Directory.CreateDirectory(outputDir); + } + catch (Exception e) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException(e.Message), + "ErrorCreatingTempDir", + ErrorCategory.InvalidData, + this)); + + return; + } + + try + { + string nuspec = string.Empty; + + // Create a nuspec + try + { + nuspec = CreateNuspec( + outputDir: outputDir, + filePath: (resourceType == ResourceType.Script) ? pathToScriptFileToPublish : pathToModuleManifestToPublish, + parsedMetadataHash: parsedMetadata, + requiredModules: out dependencies); + } + catch (Exception e) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Nuspec creation failed: {e.Message}"), + "NuspecCreationFailed", + ErrorCategory.ObjectNotFound, + this)); + + return; + } + + if (string.IsNullOrEmpty(nuspec)) + { + // nuspec creation failed. + _cmdletPassedIn.WriteVerbose("Nuspec creation failed."); + return; + } + + if (resourceType == ResourceType.Script) + { + // copy the script file to the temp directory + File.Copy(pathToScriptFileToPublish, System.IO.Path.Combine(outputDir, _pkgName + PSScriptFileExt), true); + } + else + { + try + { + // If path is pointing to a file, get the parent directory, otherwise assumption is that path is pointing to the root directory + string rootModuleDir = !string.IsNullOrEmpty(pathToModuleManifestToPublish) ? System.IO.Path.GetDirectoryName(pathToModuleManifestToPublish) : pathToModuleDirToPublish; + + // Create subdirectory structure in temp folder + foreach (string dir in Directory.GetDirectories(rootModuleDir, "*", SearchOption.AllDirectories)) + { + var dirName = dir.Substring(rootModuleDir.Length).Trim(_PathSeparators); + Directory.CreateDirectory(System.IO.Path.Combine(outputDir, dirName)); + } + + // Copy files over to temp folder + foreach (string fileNamePath in Directory.GetFiles(rootModuleDir, "*", SearchOption.AllDirectories)) + { + var fileName = fileNamePath.Substring(rootModuleDir.Length).Trim(_PathSeparators); + var newFilePath = System.IO.Path.Combine(outputDir, fileName); + + // The user may have a .nuspec defined in the module directory + // If that's the case, we will not use that file and use the .nuspec that is generated via PSGet + // The .nuspec that is already in in the output directory is the one that was generated via the CreateNuspec method + if (!File.Exists(newFilePath)) + { + File.Copy(fileNamePath, newFilePath); + } + } + } + catch (Exception e) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new ArgumentException("Error occured while creating directory to publish: " + e.Message), + "ErrorCreatingDirectoryToPublish", + ErrorCategory.InvalidOperation, + this)); + } + } + + // pack into .nupkg + if (!PackNupkg(outputDir, outputNupkgDir, nuspec, out ErrorRecord packNupkgError)) + { + _cmdletPassedIn.WriteError(packNupkgError); + // exit out of processing + return; + } + } + catch (Exception e) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + e, + $"{this.GetType()}Error", + ErrorCategory.NotSpecified, + this)); + } + finally + { + if(_callerCmdlet == CallerCmdlet.CompressPSResource) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting temporary directory '{0}'", outputDir)); + Utils.DeleteDirectory(outputDir); + } + } + } + + internal void PushResource(string Repository, bool SkipDependenciesCheck, NetworkCredential _networkCrendential) + { + try + { + PSRepositoryInfo repository = RepositorySettings.Read(new[] { Repository }, out _).FirstOrDefault(); + // Find repository + if (repository == null) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"The resource repository '{Repository}' is not a registered. Please run 'Register-PSResourceRepository' in order to publish to this repository."), + "RepositoryNotFound", + ErrorCategory.ObjectNotFound, + this)); + + return; + } + else if (repository.Uri.Scheme == Uri.UriSchemeFile && !repository.Uri.IsUnc && !Directory.Exists(repository.Uri.LocalPath)) + { + // this check to ensure valid local path is not for UNC paths (which are server based, instead of Drive based) + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"The repository '{repository.Name}' with uri: '{repository.Uri.AbsoluteUri}' is not a valid folder path which exists. If providing a file based repository, provide a repository with a path that exists."), + "repositoryPathDoesNotExist", + ErrorCategory.ObjectNotFound, + this)); + + return; + } + + _networkCredential = Utils.SetNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + + // Check if dependencies already exist within the repo if: + // 1) the resource to publish has dependencies and + // 2) the -SkipDependenciesCheck flag is not passed in + if (dependencies != null && !SkipDependenciesCheck) + { + // If error gets thrown, exit process record + if (!CheckDependenciesExist(dependencies, repository.Name)) + { + return; + } + } + + // If -DestinationPath is specified then also publish the .nupkg there + if (!string.IsNullOrWhiteSpace(DestinationPath)) + { + if (!Directory.Exists(DestinationPath)) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Destination path does not exist: '{DestinationPath}'"), + "InvalidDestinationPath", + ErrorCategory.InvalidArgument, + this)); + + return; + } + + if (!_isNupkgPathSpecified) + { + try + { + var nupkgName = _pkgName + "." + _pkgVersion.ToNormalizedString() + ".nupkg"; + var sourceFilePath = System.IO.Path.Combine(outputNupkgDir, nupkgName); + var destinationFilePath = System.IO.Path.Combine(DestinationPath, nupkgName); + + if (!File.Exists(destinationFilePath)) + { + File.Copy(sourceFilePath, destinationFilePath); + } + } + catch (Exception e) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Error moving .nupkg into destination path '{DestinationPath}' due to: '{e.Message}'."), + "ErrorMovingNupkg", + ErrorCategory.NotSpecified, + this)); + + // exit process record + return; + } + } + } + + string repositoryUri = repository.Uri.AbsoluteUri; + + if (repository.ApiVersion == PSRepositoryInfo.APIVersion.ContainerRegistry) + { + ContainerRegistryServerAPICalls containerRegistryServer = new ContainerRegistryServerAPICalls(repository, _cmdletPassedIn, _networkCredential, userAgentString); + + var pkgMetadataFile = (resourceType == ResourceType.Script) ? pathToScriptFileToPublish : pathToModuleManifestToPublish; + if (!containerRegistryServer.PushNupkgContainerRegistry(pkgMetadataFile, outputNupkgDir, _pkgName, _pkgVersion, resourceType, parsedMetadata, dependencies, out ErrorRecord pushNupkgContainerRegistryError)) + { + _cmdletPassedIn.WriteError(pushNupkgContainerRegistryError); + // exit out of processing + return; + } + } + else + { + if(_isNupkgPathSpecified) + { + 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)) + { + _cmdletPassedIn.WriteError(pushNupkgError); + // exit out of processing + return; + } + } + } + catch (Exception e) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + e, + "PublishPSResourceError", + ErrorCategory.NotSpecified, + this)); + } + finally + { + if (!_isNupkgPathSpecified) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting temporary directory '{0}'", outputDir)); + Utils.DeleteDirectory(outputDir); + } + } + } + + internal void CheckAllParameterPaths() + { + try + { + resolvedPath = _cmdletPassedIn.GetResolvedProviderPathFromPSPath(Path, out ProviderInfo provider).First(); + } + catch (MethodInvocationException) + { + // path does not exist + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new ArgumentException($"The path to the resource to {_cmdOperation.ToLower()} does not exist, point to an existing path or file of the module or script to {_cmdOperation.ToLower()}."), + "SourcePathDoesNotExist", + ErrorCategory.InvalidArgument, + this)); + } + + // Condition 1: path is to the root directory of the module to be published + // Condition 2: path is to the .psd1 or .ps1 of the module/script to be published + if (string.IsNullOrEmpty(resolvedPath)) + { + // unsupported file path + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new ArgumentException($"The path to the resource to {_cmdOperation.ToLower()} is not in the correct format or does not exist. Please provide the path of the root module " + + $"(i.e. './/') or the path to the .psd1 (i.e. './/.psd1')."), + $"Invalid{_cmdOperation}Path", + ErrorCategory.InvalidArgument, + this)); + } + else if (Directory.Exists(resolvedPath)) + { + pathToModuleDirToPublish = resolvedPath; + resourceType = ResourceType.Module; + } + else if (resolvedPath.EndsWith(PSDataFileExt, StringComparison.OrdinalIgnoreCase)) + { + pathToModuleManifestToPublish = resolvedPath; + resourceType = ResourceType.Module; + } + else if (resolvedPath.EndsWith(PSScriptFileExt, StringComparison.OrdinalIgnoreCase)) + { + pathToScriptFileToPublish = resolvedPath; + resourceType = ResourceType.Script; + } + else if (resolvedPath.EndsWith(NupkgFileExt, StringComparison.OrdinalIgnoreCase) && _isNupkgPathSpecified) + { + pathToNupkgToPublish = resolvedPath; + resourceType = ResourceType.Nupkg; + } + else + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new ArgumentException($"The {_cmdOperation.ToLower()} path provided, '{resolvedPath}', is not a valid. Please provide a path to the root module " + + $"(i.e. './/') or path to the .psd1 (i.e. './/.psd1')."), + $"Invalid{_cmdOperation}Path", + ErrorCategory.InvalidArgument, + this)); + } + + if (!String.IsNullOrEmpty(DestinationPath)) + { + string resolvedDestinationPath = _cmdletPassedIn.GetResolvedProviderPathFromPSPath(DestinationPath, out ProviderInfo provider).First(); + + if (Directory.Exists(resolvedDestinationPath)) + { + DestinationPath = resolvedDestinationPath; + } + else + { + try + { + Directory.CreateDirectory(resolvedDestinationPath); + } + catch (Exception e) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new ArgumentException($"Destination path does not exist and cannot be created: {e.Message}"), + "InvalidDestinationPath", + ErrorCategory.InvalidArgument, + this)); + } + } + } + } + + #endregion + + #region Private Methods + + private bool PackNupkg(string outputDir, string outputNupkgDir, string nuspecFile, out ErrorRecord error) + { + _cmdletPassedIn.WriteDebug("In PublishHelper::PackNupkg()"); + // Pack the module or script into a nupkg given a nuspec. + var builder = new PackageBuilder(); + try + { + var runner = new PackCommandRunner( + new PackArgs + { + CurrentDirectory = outputDir, + OutputDirectory = outputNupkgDir, + Path = nuspecFile, + Exclude = System.Array.Empty(), + Symbols = false, + Logger = NullLogger.Instance + }, + MSBuildProjectFactory.ProjectCreator, + builder); + bool success = runner.RunPackageBuild(); + + if (success) + { + _cmdletPassedIn.WriteVerbose("Successfully packed the resource into a .nupkg"); + } + else + { + error = new ErrorRecord( + new InvalidOperationException("Not able to successfully pack the resource into a .nupkg"), + "failedToPackIntoNupkg", + ErrorCategory.ObjectNotFound, + this); + + return false; + } + } + catch (Exception e) + { + error = new ErrorRecord( + new ArgumentException($"Unexpected error packing into .nupkg: '{e.Message}'."), + "ErrorPackingIntoNupkg", + ErrorCategory.NotSpecified, + this); + + // exit process record + return false; + } + + error = null; + return true; + } + + private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, out ErrorRecord error) + { + _cmdletPassedIn.WriteDebug("In PublishPSResource::PushNupkg()"); + + string fullNupkgFile; + if (_isNupkgPathSpecified) + { + fullNupkgFile = outputNupkgDir; + } + else + { + // Push the nupkg to the appropriate repository + // Pkg version is parsed from .ps1 file or .psd1 file + fullNupkgFile = System.IO.Path.Combine(outputNupkgDir, _pkgName + "." + _pkgVersion.ToNormalizedString() + ".nupkg"); + } + + // The PSGallery uses the v2 protocol still and publishes to a slightly different endpoint: + // "https://www.powershellgallery.com/api/v2/package" + // Until the PSGallery is moved onto the NuGet v3 server protocol, we'll modify the repository uri + // to accommodate for the approprate publish location. + string publishLocation = repoUri.EndsWith("/v2", StringComparison.OrdinalIgnoreCase) ? repoUri + "/package" : repoUri; + + var settings = NuGet.Configuration.Settings.LoadDefaultSettings(null, null, null); + var success = false; + + var sourceProvider = new PackageSourceProvider(settings); + if (Credential != null || _networkCredential != null) + { + InjectCredentialsToSettings(settings, sourceProvider, publishLocation); + } + + + try + { + PushRunner.Run( + settings: Settings.LoadDefaultSettings(root: null, configFileName: null, machineWideSettings: null), + sourceProvider: sourceProvider, + packagePaths: new List { fullNupkgFile }, + source: publishLocation, + apiKey: ApiKey, + symbolSource: null, + symbolApiKey: null, + timeoutSeconds: 0, + disableBuffering: false, + noSymbols: false, + noServiceEndpoint: false, // enable server endpoint + skipDuplicate: false, // if true-- if a package and version already exists, skip it and continue with the next package in the push, if any. + logger: NullLogger.Instance // nuget logger + ).GetAwaiter().GetResult(); + } + catch (HttpRequestException e) + { + _cmdletPassedIn.WriteVerbose(string.Format("Not able to publish resource to '{0}'", repoUri)); + // look in PS repo for how httpRequestExceptions are handled + + // Unfortunately there is no response message are no status codes provided with the exception and no + var ex = new ArgumentException(String.Format("Repository '{0}': {1}", repoName, e.Message)); + if (e.Message.Contains("400")) + { + if (e.Message.Contains("Api")) + { + // For ADO repositories, public and private, when ApiKey is not provided. + error = new ErrorRecord( + new ArgumentException($"Repository '{repoName}': Please try running again with the -ApiKey parameter and specific API key for the repository specified. For Azure Devops repository, set this to an arbitrary value, for example '-ApiKey AzureDevOps'"), + "400ApiKeyError", + ErrorCategory.AuthenticationError, + this); + } + else + { + error = new ErrorRecord( + ex, + "400Error", + ErrorCategory.PermissionDenied, + this); + } + } + else if (e.Message.Contains("401")) + { + if (e.Message.Contains("API")) + { + // For PSGallery when ApiKey is not provided. + error = new ErrorRecord( + new ArgumentException($"Could not publish to repository '{repoName}'. Please try running again with the -ApiKey parameter and the API key for the repository specified. Exception: '{e.Message}'"), + "401ApiKeyError", + ErrorCategory.AuthenticationError, + this); + } + else + { + // For ADO repository feeds that are public feeds, when the credentials are incorrect. + error = new ErrorRecord(new ArgumentException($"Could not publish to repository '{repoName}'. The Credential provided was incorrect. Exception: '{e.Message}'"), + "401Error", + ErrorCategory.PermissionDenied, + this); ; + } + } + else if (e.Message.Contains("403")) + { + if (repoUri.Contains("myget.org")) + { + // For myGet.org repository feeds when the ApiKey is missing or incorrect. + error = new ErrorRecord( + new ArgumentException($"Could not publish to repository '{repoName}'. The ApiKey provided is incorrect or missing. Please try running again with the -ApiKey parameter and correct API key value for the repository. Exception: '{e.Message}'"), + "403Error", + ErrorCategory.PermissionDenied, + this); + } + else if (repoUri.Contains(".jfrog.io")) + { + // For JFrog Artifactory repository feeds when the ApiKey is provided, whether correct or incorrect, as JFrog does not require -ApiKey (but does require ApiKey to be present as password to -Credential). + error = new ErrorRecord( + new ArgumentException($"Could not publish to repository '{repoName}'. The ApiKey provided is not needed for JFrog Artifactory. Please try running again without the -ApiKey parameter but ensure that -Credential is provided with ApiKey as password. Exception: '{e.Message}'"), + "403Error", + ErrorCategory.PermissionDenied, + this); + } + else + { + error = new ErrorRecord( + ex, + "403Error", + ErrorCategory.PermissionDenied, + this); + } + } + else if (e.Message.Contains("409")) + { + error = new ErrorRecord( + ex, + "409Error", + ErrorCategory.PermissionDenied, this); + } + else + { + error = new ErrorRecord( + ex, + "HTTPRequestError", + ErrorCategory.PermissionDenied, + this); + } + + return success; + } + catch (NuGet.Protocol.Core.Types.FatalProtocolException e) + { + // for ADO repository feeds that are private feeds the error thrown is different and the 401 is in the inner exception message + if (e.InnerException.Message.Contains("401")) + { + error = new ErrorRecord( + new ArgumentException($"Could not publish to repository '{repoName}'. The Credential provided was incorrect. Exception '{e.InnerException.Message}'"), + "401FatalProtocolError", + ErrorCategory.AuthenticationError, + this); + } + else + { + error = new ErrorRecord( + new ArgumentException($"Repository '{repoName}': {e.InnerException.Message}"), + "ProtocolFailError", + ErrorCategory.ProtocolError, + this); + } + + return success; + } + catch (Exception e) + { + _cmdletPassedIn.WriteVerbose($"Not able to publish resource to '{repoUri}'"); + error = new ErrorRecord( + new ArgumentException(e.Message), + "PushNupkgError", + ErrorCategory.InvalidResult, + this); + + return success; + } + + _cmdletPassedIn.WriteVerbose(string.Format("Successfully published the resource to '{0}'", repoUri)); + error = null; + success = true; + + return success; + } + + private void InjectCredentialsToSettings(ISettings settings, IPackageSourceProvider sourceProvider, string source) + { + _cmdletPassedIn.WriteDebug("In PublishPSResource::InjectCredentialsToSettings()"); + if (Credential == null && _networkCredential == null) + { + return; + } + + var packageSource = sourceProvider.LoadPackageSources().FirstOrDefault(s => s.Source == source); + if (packageSource != null) + { + if (!packageSource.IsEnabled) + { + packageSource.IsEnabled = true; + } + } + + + var networkCred = Credential == null ? _networkCredential : Credential.GetNetworkCredential(); + string key; + + if (packageSource == null) + + { + key = "_" + Guid.NewGuid().ToString().Replace("-", ""); + settings.AddOrUpdate( + ConfigurationConstants.PackageSources, + new SourceItem(key, source)); + } + else + { + key = packageSource.Name; + } + + settings.AddOrUpdate( + ConfigurationConstants.CredentialsSectionName, + new CredentialsItem( + key, + networkCred.UserName, + networkCred.Password, + isPasswordClearText: true, + String.Empty)); + } + + private string CreateNuspec( + string outputDir, + string filePath, + Hashtable parsedMetadataHash, + out Hashtable requiredModules) + { + _cmdletPassedIn.WriteDebug("In PublishHelper::CreateNuspec()"); + + bool isModule = resourceType != ResourceType.Script; + requiredModules = new Hashtable(); + + if (parsedMetadataHash == null || parsedMetadataHash.Count == 0) + { + _cmdletPassedIn.WriteError(new ErrorRecord(new ArgumentException("Hashtable provided with package metadata was null or empty"), + "PackageMetadataHashtableNullOrEmptyError", + ErrorCategory.ReadError, + this)); + + return string.Empty; + } + + // now we have parsedMetadatahash to fill out the nuspec information + var nameSpaceUri = "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"; + var doc = new XmlDocument(); + + // xml declaration is recommended, but not mandatory + XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "utf-8", null); + XmlElement root = doc.DocumentElement; + doc.InsertBefore(xmlDeclaration, root); + + // create top-level elements + XmlElement packageElement = doc.CreateElement("package", nameSpaceUri); + XmlElement metadataElement = doc.CreateElement("metadata", nameSpaceUri); + + Dictionary metadataElementsDictionary = new Dictionary(); + + // id is mandatory + metadataElementsDictionary.Add("id", _pkgName); + + string version; + if (parsedMetadataHash.ContainsKey("moduleversion")) + { + version = parsedMetadataHash["moduleversion"].ToString(); + } + else if (parsedMetadataHash.ContainsKey("version")) + { + version = parsedMetadataHash["version"].ToString(); + } + else + { + // no version is specified for the nuspec + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException("There is no package version specified. Please specify a version before publishing."), + "NoVersionFound", + ErrorCategory.InvalidArgument, + this)); + + return string.Empty; + } + + // Look for Prerelease tag and then process any Tags in PrivateData > PSData + if (isModule) + { + if (parsedMetadataHash.ContainsKey("PrivateData")) + { + if (parsedMetadataHash["PrivateData"] is Hashtable privateData && + privateData.ContainsKey("PSData")) + { + if (privateData["PSData"] is Hashtable psData) + { + if (psData.ContainsKey("prerelease") && psData["prerelease"] is string preReleaseVersion) + { + if (!string.IsNullOrEmpty(preReleaseVersion)) + { + version = string.Format(@"{0}-{1}", version, preReleaseVersion); + } + } + + if (psData.ContainsKey("licenseuri") && psData["licenseuri"] is string licenseUri) + + { + metadataElementsDictionary.Add("licenseUrl", licenseUri.Trim()); + } + + if (psData.ContainsKey("projecturi") && psData["projecturi"] is string projectUri) + { + metadataElementsDictionary.Add("projectUrl", projectUri.Trim()); + } + + if (psData.ContainsKey("iconuri") && psData["iconuri"] is string iconUri) + { + metadataElementsDictionary.Add("iconUrl", iconUri.Trim()); + } + + if (psData.ContainsKey("releasenotes")) + { + if (psData["releasenotes"] is string releaseNotes) + { + metadataElementsDictionary.Add("releaseNotes", releaseNotes.Trim()); + } + else if (psData["releasenotes"] is string[] releaseNotesArr) + { + metadataElementsDictionary.Add("releaseNotes", string.Join("\n", releaseNotesArr)); + } + } + + // defaults to false + // Value for requireAcceptLicense key needs to be a lowercase string representation of the boolean for it to be correctly parsed from psData file. + + string requireLicenseAcceptance = psData.ContainsKey("requirelicenseacceptance") ? psData["requirelicenseacceptance"].ToString().ToLower() : "false"; + + metadataElementsDictionary.Add("requireLicenseAcceptance", requireLicenseAcceptance); + + + if (psData.ContainsKey("Tags") && psData["Tags"] is Array manifestTags) + { + var tagArr = new List(); + foreach (string tag in manifestTags) + { + tagArr.Add(tag); + } + parsedMetadataHash["tags"] = string.Join(" ", tagArr.ToArray()); + } + } + } + } + } + else + { + if (parsedMetadataHash.ContainsKey("licenseuri") && parsedMetadataHash["licenseuri"] is Uri licenseUri) + + { + metadataElementsDictionary.Add("licenseUrl", licenseUri.ToString().Trim()); + } + + if (parsedMetadataHash.ContainsKey("projecturi") && parsedMetadataHash["projecturi"] is Uri projectUri) + { + metadataElementsDictionary.Add("projectUrl", projectUri.ToString().Trim()); + } + + if (parsedMetadataHash.ContainsKey("iconuri") && parsedMetadataHash["iconuri"] is Uri iconUri) + { + metadataElementsDictionary.Add("iconUrl", iconUri.ToString().Trim()); + } + + if (parsedMetadataHash.ContainsKey("releaseNotes")) + { + if (parsedMetadataHash["releasenotes"] is string releaseNotes) + { + metadataElementsDictionary.Add("releaseNotes", releaseNotes.Trim()); + } + else if (parsedMetadataHash["releasenotes"] is string[] releaseNotesArr) + { + metadataElementsDictionary.Add("releaseNotes", string.Join("\n", releaseNotesArr)); + } + } + } + + + if (NuGetVersion.TryParse(version, out _pkgVersion)) + { + metadataElementsDictionary.Add("version", _pkgVersion.ToNormalizedString()); + } + + if (parsedMetadataHash.ContainsKey("author")) + { + metadataElementsDictionary.Add("authors", parsedMetadataHash["author"].ToString().Trim()); + } + + if (parsedMetadataHash.ContainsKey("companyname")) + { + metadataElementsDictionary.Add("owners", parsedMetadataHash["companyname"].ToString().Trim()); + } + + if (parsedMetadataHash.ContainsKey("description")) + { + metadataElementsDictionary.Add("description", parsedMetadataHash["description"].ToString().Trim()); + } + + if (parsedMetadataHash.ContainsKey("copyright")) + { + metadataElementsDictionary.Add("copyright", parsedMetadataHash["copyright"].ToString().Trim()); + } + + string tags = (resourceType == ResourceType.Script) ? "PSScript" : "PSModule"; + if (parsedMetadataHash.ContainsKey("tags") && parsedMetadataHash["tags"] != null) + { + if (parsedMetadataHash["tags"] is string[]) + { + string[] tagsArr = parsedMetadataHash["tags"] as string[]; + tags += " " + String.Join(" ", tagsArr); + } + else if (parsedMetadataHash["tags"] is string) + { + tags += " " + parsedMetadataHash["tags"].ToString().Trim(); + } + } + + metadataElementsDictionary.Add("tags", tags); + + + // Example nuspec: + /* + + + + System.Management.Automation + 1.0.0 + Microsoft + Microsoft,PowerShell + false + MIT + https://licenses.nuget.org/MIT + Powershell_black_64.png + https://github.com/PowerShell/PowerShell + Example description here + Copyright (c) Microsoft Corporation. All rights reserved. + en-US + PowerShell + + + + + + + + + */ + + foreach (var key in metadataElementsDictionary.Keys) + { + if (metadataElementsDictionary.TryGetValue(key, out string elementInnerText)) + { + XmlElement element = doc.CreateElement(key, nameSpaceUri); + element.InnerText = elementInnerText; + metadataElement.AppendChild(element); + } + else + { + _cmdletPassedIn.WriteVerbose(string.Format("Creating XML element failed. Unable to get value from key '{0}'.", key)); + } + } + + requiredModules = ParseRequiredModules(parsedMetadataHash); + if (requiredModules != null) + { + XmlElement dependenciesElement = doc.CreateElement("dependencies", nameSpaceUri); + + foreach (string dependencyName in requiredModules.Keys) + { + XmlElement element = doc.CreateElement("dependency", nameSpaceUri); + + element.SetAttribute("id", dependencyName); + string dependencyVersion = requiredModules[dependencyName].ToString(); + if (!string.IsNullOrEmpty(dependencyVersion)) + { + element.SetAttribute("version", requiredModules[dependencyName].ToString()); + } + + dependenciesElement.AppendChild(element); + } + metadataElement.AppendChild(dependenciesElement); + } + + packageElement.AppendChild(metadataElement); + doc.AppendChild(packageElement); + + var nuspecFullName = System.IO.Path.Combine(outputDir, _pkgName + ".nuspec"); + doc.Save(nuspecFullName); + + _cmdletPassedIn.WriteVerbose("The newly created nuspec is: " + nuspecFullName); + + return nuspecFullName; + } + + private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash) + { + _cmdletPassedIn.WriteDebug("In PublishHelper::ParseRequiredModules()"); + + if (!parsedMetadataHash.ContainsKey("requiredmodules")) + { + return null; + } + + LanguagePrimitives.TryConvertTo(parsedMetadataHash["requiredmodules"], out object[] requiredModules); + + // Required modules can be: + // a. An array of hash tables of module name and version + // b. A single hash table of module name and version + // c. A string array of module names + // d. A single string module name + + var dependenciesHash = new Hashtable(); + foreach (var reqModule in requiredModules) + { + if (LanguagePrimitives.TryConvertTo(reqModule, out Hashtable moduleHash)) + { + string moduleName = moduleHash["ModuleName"] as string; + + if (moduleHash.ContainsKey("ModuleVersion")) + { + dependenciesHash.Add(moduleName, moduleHash["ModuleVersion"]); + } + else if (moduleHash.ContainsKey("RequiredVersion")) + { + dependenciesHash.Add(moduleName, moduleHash["RequiredVersion"]); + } + else + { + dependenciesHash.Add(moduleName, string.Empty); + } + } + else if (LanguagePrimitives.TryConvertTo(reqModule, out string moduleName)) + { + dependenciesHash.Add(moduleName, string.Empty); + } + } + + var externalModuleDeps = parsedMetadataHash.ContainsKey("ExternalModuleDependencies") ? + parsedMetadataHash["ExternalModuleDependencies"] : null; + + if (externalModuleDeps != null && LanguagePrimitives.TryConvertTo(externalModuleDeps, out string[] externalModuleNames)) + { + foreach (var extModName in externalModuleNames) + { + if (dependenciesHash.ContainsKey(extModName)) + { + dependenciesHash.Remove(extModName); + } + } + } + + return dependenciesHash; + } + + private bool CheckDependenciesExist(Hashtable dependencies, string repositoryName) + { + _cmdletPassedIn.WriteDebug("In PublishHelper::CheckDependenciesExist()"); + + // Check to see that all dependencies are in the repository + // Searches for each dependency in the repository the pkg is being pushed to, + // If the dependency is not there, error + foreach (DictionaryEntry dependency in dependencies) + { + // Need to make individual calls since we're look for exact version numbers or ranges. + var depName = dependency.Key as string; + // test version + string depVersion = dependencies[depName] as string; + depVersion = string.IsNullOrWhiteSpace(depVersion) ? "*" : depVersion; + + if (!Utils.TryGetVersionType( + version: depVersion, + nugetVersion: out NuGetVersion nugetVersion, + versionRange: out VersionRange versionRange, + versionType: out VersionType versionType, + error: out string error)) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new ArgumentException(error), + "IncorrectVersionFormat", + ErrorCategory.InvalidArgument, + this)); + } + + // Search for and return the dependency if it's in the repository. + FindHelper findHelper = new FindHelper(_cancellationToken, _cmdletPassedIn, _networkCredential); + + var repository = new[] { repositoryName }; + // Note: we set prerelease argument for FindByResourceName() to true because if no version is specified we want latest version (including prerelease). + // If version is specified it will get that one. There is also no way to specify a prerelease flag with RequiredModules hashtable of dependency so always try to get latest version. + var dependencyFound = findHelper.FindByResourceName(new string[] { depName }, ResourceType.Module, versionRange, nugetVersion, versionType, depVersion, prerelease: true, tag: null, repository, includeDependencies: false, suppressErrors: true); + if (dependencyFound == null || !dependencyFound.Any()) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Dependency '{depName}' was not found in repository '{repositoryName}'. Make sure the dependency is published to the repository before {_cmdOperation.ToLower()} this module."), + "DependencyNotFound", + ErrorCategory.ObjectNotFound, + this)); + + return false; + } + + } + + return true; + } + + #endregion + } +} diff --git a/src/code/PublishPSResource.cs b/src/code/PublishPSResource.cs index d3ba80d4a..620de3b7e 100644 --- a/src/code/PublishPSResource.cs +++ b/src/code/PublishPSResource.cs @@ -2,21 +2,10 @@ // Licensed under the MIT License. using Microsoft.PowerShell.PSResourceGet.UtilClasses; -using NuGet.Commands; -using NuGet.Common; -using NuGet.Configuration; -using NuGet.Packaging; -using NuGet.Versioning; using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Management.Automation; using System.Net; -using System.Net.Http; using System.Threading; -using System.Xml; namespace Microsoft.PowerShell.PSResourceGet.Cmdlets { @@ -31,6 +20,9 @@ public sealed class PublishPSResource : PSCmdlet { #region Parameters + private const string PathParameterSet = "PathParameterSet"; + private const string NupkgPathParameterSet = "NupkgPathParameterSet"; + /// /// Specifies the API key that you want to use to publish a module to the online gallery. /// @@ -50,7 +42,7 @@ public sealed class PublishPSResource : PSCmdlet /// Specifies the path to the resource that you want to publish. This parameter accepts the path to the folder that contains the resource. /// Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory (.). /// - [Parameter (Mandatory = true, Position = 0, HelpMessage = "Path to the resource to be published.")] + [Parameter (Mandatory = true, Position = 0, ParameterSetName = PathParameterSet, HelpMessage = "Path to the resource to be published.")] [ValidateNotNullOrEmpty] public string Path { get; set; } @@ -119,24 +111,18 @@ public PSCredential ProxyCredential { } } + [Parameter(Mandatory = true, ParameterSetName = NupkgPathParameterSet, HelpMessage = "Path to the resource to be published.")] + [ValidateNotNullOrEmpty] + public string NupkgPath { get; set; } + #endregion #region Members - private string resolvedPath; private CancellationToken _cancellationToken; - private NuGetVersion _pkgVersion; - private string _pkgName; - private static char[] _PathSeparators = new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar }; - public const string PSDataFileExt = ".psd1"; - public const string PSScriptFileExt = ".ps1"; - private const string PSScriptInfoCommentString = "<#PSScriptInfo"; - private string pathToScriptFileToPublish = string.Empty; - private string pathToModuleManifestToPublish = string.Empty; - private string pathToModuleDirToPublish = string.Empty; - private ResourceType resourceType = ResourceType.None; private NetworkCredential _networkCredential; - string userAgentString = UserAgentInfo.UserAgentString(); + private bool _isNupkgPathSpecified = false; + private PublishHelper _publishHelper; #endregion @@ -148,1046 +134,45 @@ protected override void BeginProcessing() _networkCredential = Credential != null ? new NetworkCredential(Credential.UserName, Credential.Password) : null; - // Create a respository story (the PSResourceRepository.xml file) if it does not already exist - // This is to create a better experience for those who have just installed v3 and want to get up and running quickly - RepositorySettings.CheckRepositoryStore(); - - try - { - resolvedPath = GetResolvedProviderPathFromPSPath(Path, out ProviderInfo provider).First(); - } - catch (MethodInvocationException) + if (!string.IsNullOrEmpty(NupkgPath)) { - // path does not exist - ThrowTerminatingError(new ErrorRecord( - new ArgumentException("The path to the resource to publish does not exist, point to an existing path or file of the module or script to publish."), - "SourcePathDoesNotExist", - ErrorCategory.InvalidArgument, - this)); + _isNupkgPathSpecified = true; + Path = NupkgPath; } - // Condition 1: path is to the root directory of the module to be published - // Condition 2: path is to the .psd1 or .ps1 of the module/script to be published - if (string.IsNullOrEmpty(resolvedPath)) - { - // unsupported file path - ThrowTerminatingError(new ErrorRecord( - new ArgumentException("The path to the resource to publish is not in the correct format or does not exist. Please provide the path of the root module " + - "(i.e. './/') or the path to the .psd1 (i.e. './/.psd1')."), - "InvalidPublishPath", - ErrorCategory.InvalidArgument, - this)); - } - else if (Directory.Exists(resolvedPath)) - { - pathToModuleDirToPublish = resolvedPath; - resourceType = ResourceType.Module; - } - else if (resolvedPath.EndsWith(PSDataFileExt, StringComparison.OrdinalIgnoreCase)) - { - pathToModuleManifestToPublish = resolvedPath; - resourceType = ResourceType.Module; - } - else if (resolvedPath.EndsWith(PSScriptFileExt, StringComparison.OrdinalIgnoreCase)) - { - pathToScriptFileToPublish = resolvedPath; - resourceType = ResourceType.Script; - } - else { - ThrowTerminatingError(new ErrorRecord( - new ArgumentException($"The publish path provided, '{resolvedPath}', is not a valid. Please provide a path to the root module " + - "(i.e. './/') or path to the .psd1 (i.e. './/.psd1')."), - "InvalidPublishPath", - ErrorCategory.InvalidArgument, - this)); - } - - if (!String.IsNullOrEmpty(DestinationPath)) - { - string resolvedDestinationPath = GetResolvedProviderPathFromPSPath(DestinationPath, out ProviderInfo provider).First(); + // Create a respository story (the PSResourceRepository.xml file) if it does not already exist + // This is to create a better experience for those who have just installed v3 and want to get up and running quickly + RepositorySettings.CheckRepositoryStore(); - if (Directory.Exists(resolvedDestinationPath)) - { - DestinationPath = resolvedDestinationPath; - } - else - { - try - { - Directory.CreateDirectory(resolvedDestinationPath); - } - catch (Exception e) - { - ThrowTerminatingError(new ErrorRecord( - new ArgumentException($"Destination path does not exist and cannot be created: {e.Message}"), - "InvalidDestinationPath", - ErrorCategory.InvalidArgument, - this)); - } - } - } + _publishHelper = new PublishHelper( + this, + Credential, + ApiKey, + Path, + DestinationPath, + SkipModuleManifestValidate, + _cancellationToken, + _isNupkgPathSpecified); + + _publishHelper.CheckAllParameterPaths(); } protected override void EndProcessing() { - // Returns the name of the file or the name of the directory, depending on path - if (!ShouldProcess(string.Format("Publish resource '{0}' from the machine", resolvedPath))) - { - WriteVerbose("ShouldProcess is set to false."); - return; - } - - Hashtable parsedMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase); - if (resourceType == ResourceType.Script) - { - if (!PSScriptFileInfo.TryTestPSScriptFileInfo( - scriptFileInfoPath: pathToScriptFileToPublish, - parsedScript: out PSScriptFileInfo scriptToPublish, - out ErrorRecord[] errors, - out string[] _ - )) - { - foreach (ErrorRecord error in errors) - { - WriteError(error); - } - - return; - } - - parsedMetadata = scriptToPublish.ToHashtable(); - - _pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToScriptFileToPublish); - } - else - { - if (!string.IsNullOrEmpty(pathToModuleManifestToPublish)) - { - _pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToModuleManifestToPublish); - } - else { - // Search for module manifest - foreach (FileInfo file in new DirectoryInfo(pathToModuleDirToPublish).EnumerateFiles()) - { - if (file.Name.EndsWith(PSDataFileExt, StringComparison.OrdinalIgnoreCase)) - { - pathToModuleManifestToPublish = file.FullName; - _pkgName = System.IO.Path.GetFileNameWithoutExtension(file.Name); - - break; - } - } - } - - // Validate that there's a module manifest - if (!File.Exists(pathToModuleManifestToPublish)) - { - WriteError(new ErrorRecord( - new ArgumentException($"No file with a .psd1 extension was found in '{pathToModuleManifestToPublish}'. Please specify a path to a valid module manifest."), - "moduleManifestNotFound", - ErrorCategory.ObjectNotFound, - this)); - - return; - } - - // The Test-ModuleManifest currently cannot process UNC paths. Disabling verification for now. - if ((new Uri(pathToModuleManifestToPublish)).IsUnc) - SkipModuleManifestValidate = true; - // Validate that the module manifest has correct data - if (! SkipModuleManifestValidate && - ! Utils.ValidateModuleManifest(pathToModuleManifestToPublish, out string errorMsg)) - { - ThrowTerminatingError(new ErrorRecord( - new PSInvalidOperationException(errorMsg), - "InvalidModuleManifest", - ErrorCategory.InvalidOperation, - this)); - } - - if (!Utils.TryReadManifestFile( - manifestFilePath: pathToModuleManifestToPublish, - manifestInfo: out parsedMetadata, - error: out Exception manifestReadError)) - { - WriteError(new ErrorRecord( - manifestReadError, - "ManifestFileReadParseForContainerRegistryPublishError", - ErrorCategory.ReadError, - this)); - - return; - } - } - - // Create a temp folder to push the nupkg to and delete it later - string outputDir = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); - try - { - Directory.CreateDirectory(outputDir); - } - catch (Exception e) - { - WriteError(new ErrorRecord( - new ArgumentException(e.Message), - "ErrorCreatingTempDir", - ErrorCategory.InvalidData, - this)); - - return; - } - - try - { - // Create a nuspec - Hashtable dependencies; - string nuspec = string.Empty; - try - { - nuspec = CreateNuspec( - outputDir: outputDir, - filePath: (resourceType == ResourceType.Script) ? pathToScriptFileToPublish : pathToModuleManifestToPublish, - parsedMetadataHash: parsedMetadata, - requiredModules: out dependencies); - } - catch (Exception e) - { - WriteError(new ErrorRecord( - new ArgumentException($"Nuspec creation failed: {e.Message}"), - "NuspecCreationFailed", - ErrorCategory.ObjectNotFound, - this)); - - return; - } - - if (string.IsNullOrEmpty(nuspec)) - { - // nuspec creation failed. - WriteVerbose("Nuspec creation failed."); - return; - } - - // Find repository - PSRepositoryInfo repository = RepositorySettings.Read(new[] { Repository }, out string[] _).FirstOrDefault(); - if (repository == null) - { - WriteError(new ErrorRecord( - new ArgumentException($"The resource repository '{Repository}' is not a registered. Please run 'Register-PSResourceRepository' in order to publish to this repository."), - "RepositoryNotFound", - ErrorCategory.ObjectNotFound, - this)); - - return; - } - else if(repository.Uri.Scheme == Uri.UriSchemeFile && !repository.Uri.IsUnc && !Directory.Exists(repository.Uri.LocalPath)) - { - // this check to ensure valid local path is not for UNC paths (which are server based, instead of Drive based) - WriteError(new ErrorRecord( - new ArgumentException($"The repository '{repository.Name}' with uri: '{repository.Uri.AbsoluteUri}' is not a valid folder path which exists. If providing a file based repository, provide a repository with a path that exists."), - "repositoryPathDoesNotExist", - ErrorCategory.ObjectNotFound, - this)); - - return; - } - - _networkCredential = Utils.SetNetworkCredential(repository, _networkCredential, this); - - // Check if dependencies already exist within the repo if: - // 1) the resource to publish has dependencies and - // 2) the -SkipDependenciesCheck flag is not passed in - if (dependencies != null && !SkipDependenciesCheck) - { - // If error gets thrown, exit process record - if (!CheckDependenciesExist(dependencies, repository.Name)) - { - return; - } - } - - if (resourceType == ResourceType.Script) - { - // copy the script file to the temp directory - File.Copy(pathToScriptFileToPublish, System.IO.Path.Combine(outputDir, _pkgName + PSScriptFileExt), true); - } - else - { - try - { - // If path is pointing to a file, get the parent directory, otherwise assumption is that path is pointing to the root directory - string rootModuleDir = !string.IsNullOrEmpty(pathToModuleManifestToPublish) ? System.IO.Path.GetDirectoryName(pathToModuleManifestToPublish) : pathToModuleDirToPublish; - - // Create subdirectory structure in temp folder - foreach (string dir in Directory.GetDirectories(rootModuleDir, "*", SearchOption.AllDirectories)) - { - var dirName = dir.Substring(rootModuleDir.Length).Trim(_PathSeparators); - Directory.CreateDirectory(System.IO.Path.Combine(outputDir, dirName)); - } - - // Copy files over to temp folder - foreach (string fileNamePath in Directory.GetFiles(rootModuleDir, "*", SearchOption.AllDirectories)) - { - var fileName = fileNamePath.Substring(rootModuleDir.Length).Trim(_PathSeparators); - var newFilePath = System.IO.Path.Combine(outputDir, fileName); - - // The user may have a .nuspec defined in the module directory - // If that's the case, we will not use that file and use the .nuspec that is generated via PSGet - // The .nuspec that is already in in the output directory is the one that was generated via the CreateNuspec method - if (!File.Exists(newFilePath)) - { - File.Copy(fileNamePath, newFilePath); - } - } - } - catch (Exception e) - { - ThrowTerminatingError(new ErrorRecord( - new ArgumentException("Error occured while creating directory to publish: " + e.Message), - "ErrorCreatingDirectoryToPublish", - ErrorCategory.InvalidOperation, - this)); - } - } - - var outputNupkgDir = System.IO.Path.Combine(outputDir, "nupkg"); - - // pack into .nupkg - if (!PackNupkg(outputDir, outputNupkgDir, nuspec, out ErrorRecord packNupkgError)) - { - WriteError(packNupkgError); - // exit out of processing - return; - } - - // If -DestinationPath is specified then also publish the .nupkg there - if (!string.IsNullOrWhiteSpace(DestinationPath)) - { - if (!Directory.Exists(DestinationPath)) - { - WriteError(new ErrorRecord( - new ArgumentException($"Destination path does not exist: '{DestinationPath}'"), - "InvalidDestinationPath", - ErrorCategory.InvalidArgument, - this)); - - return; - } - - try - { - var nupkgName = _pkgName + "." + _pkgVersion.ToNormalizedString() + ".nupkg"; - File.Copy(System.IO.Path.Combine(outputNupkgDir, nupkgName), System.IO.Path.Combine(DestinationPath, nupkgName)); - } - catch (Exception e) - { - WriteError(new ErrorRecord( - new ArgumentException($"Error moving .nupkg into destination path '{DestinationPath}' due to: '{e.Message}'."), - "ErrorMovingNupkg", - ErrorCategory.NotSpecified, - this)); - - // exit process record - return; - } - } - - string repositoryUri = repository.Uri.AbsoluteUri; - - if (repository.ApiVersion == PSRepositoryInfo.APIVersion.ContainerRegistry) - { - ContainerRegistryServerAPICalls containerRegistryServer = new ContainerRegistryServerAPICalls(repository, this, _networkCredential, userAgentString); - - var pkgMetadataFile = (resourceType == ResourceType.Script) ? pathToScriptFileToPublish : pathToModuleManifestToPublish; - if (!containerRegistryServer.PushNupkgContainerRegistry(pkgMetadataFile, outputNupkgDir, _pkgName, _pkgVersion, resourceType, parsedMetadata, dependencies, out ErrorRecord pushNupkgContainerRegistryError)) - { - WriteError(pushNupkgContainerRegistryError); - // exit out of processing - return; - } - } - else - { - // 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)) - { - WriteError(pushNupkgError); - // exit out of processing - return; - } - } - } - catch (Exception e) - { - ThrowTerminatingError(new ErrorRecord( - e, - "PublishPSResourceError", - ErrorCategory.NotSpecified, - this)); - } - finally - { - WriteVerbose(string.Format("Deleting temporary directory '{0}'", outputDir)); - - Utils.DeleteDirectory(outputDir); - } - - } - #endregion - - #region Private methods - - private string CreateNuspec( - string outputDir, - string filePath, - Hashtable parsedMetadataHash, - out Hashtable requiredModules) - { - WriteDebug("In PublishPSResource::CreateNuspec()"); - bool isModule = resourceType != ResourceType.Script; - requiredModules = new Hashtable(); - - if (parsedMetadataHash == null || parsedMetadataHash.Count == 0) - { - WriteError(new ErrorRecord(new ArgumentException("Hashtable provided with package metadata was null or empty"), - "PackageMetadataHashtableNullOrEmptyError", - ErrorCategory.ReadError, - this)); - - return string.Empty; - } - - // now we have parsedMetadatahash to fill out the nuspec information - var nameSpaceUri = "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"; - var doc = new XmlDocument(); - - // xml declaration is recommended, but not mandatory - XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "utf-8", null); - XmlElement root = doc.DocumentElement; - doc.InsertBefore(xmlDeclaration, root); - - // create top-level elements - XmlElement packageElement = doc.CreateElement("package", nameSpaceUri); - XmlElement metadataElement = doc.CreateElement("metadata", nameSpaceUri); - - Dictionary metadataElementsDictionary = new Dictionary(); - - // id is mandatory - metadataElementsDictionary.Add("id", _pkgName); - - string version; - if (parsedMetadataHash.ContainsKey("moduleversion")) - { - version = parsedMetadataHash["moduleversion"].ToString(); - } - else if (parsedMetadataHash.ContainsKey("version")) - { - version = parsedMetadataHash["version"].ToString(); - } - else - { - // no version is specified for the nuspec - WriteError(new ErrorRecord( - new ArgumentException("There is no package version specified. Please specify a version before publishing."), - "NoVersionFound", - ErrorCategory.InvalidArgument, - this)); - - return string.Empty; - } - - // Look for Prerelease tag and then process any Tags in PrivateData > PSData - if (isModule) - { - if (parsedMetadataHash.ContainsKey("PrivateData")) - { - if (parsedMetadataHash["PrivateData"] is Hashtable privateData && - privateData.ContainsKey("PSData")) - { - if (privateData["PSData"] is Hashtable psData) - { - if (psData.ContainsKey("prerelease") && psData["prerelease"] is string preReleaseVersion) - { - if (!string.IsNullOrEmpty(preReleaseVersion)) - { - version = string.Format(@"{0}-{1}", version, preReleaseVersion); - } - } - - if (psData.ContainsKey("licenseuri") && psData["licenseuri"] is string licenseUri) - - { - metadataElementsDictionary.Add("licenseUrl", licenseUri.Trim()); - } - - if (psData.ContainsKey("projecturi") && psData["projecturi"] is string projectUri) - { - metadataElementsDictionary.Add("projectUrl", projectUri.Trim()); - } - - if (psData.ContainsKey("iconuri") && psData["iconuri"] is string iconUri) - { - metadataElementsDictionary.Add("iconUrl", iconUri.Trim()); - } - - if (psData.ContainsKey("releasenotes")) - { - if (psData["releasenotes"] is string releaseNotes) - { - metadataElementsDictionary.Add("releaseNotes", releaseNotes.Trim()); - } - else if (psData["releasenotes"] is string[] releaseNotesArr) - { - metadataElementsDictionary.Add("releaseNotes", string.Join("\n", releaseNotesArr)); - } - } - - // defaults to false - // Value for requireAcceptLicense key needs to be a lowercase string representation of the boolean for it to be correctly parsed from psData file. - - string requireLicenseAcceptance = psData.ContainsKey("requirelicenseacceptance") ? psData["requirelicenseacceptance"].ToString().ToLower() : "false"; - - metadataElementsDictionary.Add("requireLicenseAcceptance", requireLicenseAcceptance); - - - if (psData.ContainsKey("Tags") && psData["Tags"] is Array manifestTags) - { - var tagArr = new List(); - foreach (string tag in manifestTags) - { - tagArr.Add(tag); - } - parsedMetadataHash["tags"] = string.Join(" ", tagArr.ToArray()); - } - } - } - } - } - else - { - if (parsedMetadataHash.ContainsKey("licenseuri") && parsedMetadataHash["licenseuri"] is Uri licenseUri) - - { - metadataElementsDictionary.Add("licenseUrl", licenseUri.ToString().Trim()); - } - - if (parsedMetadataHash.ContainsKey("projecturi") && parsedMetadataHash["projecturi"] is Uri projectUri) - { - metadataElementsDictionary.Add("projectUrl", projectUri.ToString().Trim()); - } - - if (parsedMetadataHash.ContainsKey("iconuri") && parsedMetadataHash["iconuri"] is Uri iconUri) - { - metadataElementsDictionary.Add("iconUrl", iconUri.ToString().Trim()); - } - - if (parsedMetadataHash.ContainsKey("releaseNotes")) - { - if (parsedMetadataHash["releasenotes"] is string releaseNotes) - { - metadataElementsDictionary.Add("releaseNotes", releaseNotes.Trim()); - } - else if (parsedMetadataHash["releasenotes"] is string[] releaseNotesArr) - { - metadataElementsDictionary.Add("releaseNotes", string.Join("\n", releaseNotesArr)); - } - } - } - - - if (NuGetVersion.TryParse(version, out _pkgVersion)) - { - metadataElementsDictionary.Add("version", _pkgVersion.ToNormalizedString()); - } - - if (parsedMetadataHash.ContainsKey("author")) - { - metadataElementsDictionary.Add("authors", parsedMetadataHash["author"].ToString().Trim()); - } - - if (parsedMetadataHash.ContainsKey("companyname")) - { - metadataElementsDictionary.Add("owners", parsedMetadataHash["companyname"].ToString().Trim()); - } - - if (parsedMetadataHash.ContainsKey("description")) - { - metadataElementsDictionary.Add("description", parsedMetadataHash["description"].ToString().Trim()); - } - - if (parsedMetadataHash.ContainsKey("copyright")) - { - metadataElementsDictionary.Add("copyright", parsedMetadataHash["copyright"].ToString().Trim()); - } - - string tags = (resourceType == ResourceType.Script) ? "PSScript" : "PSModule"; - if (parsedMetadataHash.ContainsKey("tags") && parsedMetadataHash["tags"] != null) - { - if (parsedMetadataHash["tags"] is string[]) - { - string[] tagsArr = parsedMetadataHash["tags"] as string[]; - tags += " " + String.Join(" ", tagsArr); - } - else if (parsedMetadataHash["tags"] is string) - { - tags += " " + parsedMetadataHash["tags"].ToString().Trim(); - } - } - - metadataElementsDictionary.Add("tags", tags); - - - // Example nuspec: - /* - - - - System.Management.Automation - 1.0.0 - Microsoft - Microsoft,PowerShell - false - MIT - https://licenses.nuget.org/MIT - Powershell_black_64.png - https://github.com/PowerShell/PowerShell - Example description here - Copyright (c) Microsoft Corporation. All rights reserved. - en-US - PowerShell - - - - - - - - - */ - - foreach (var key in metadataElementsDictionary.Keys) - { - if (metadataElementsDictionary.TryGetValue(key, out string elementInnerText)) - { - XmlElement element = doc.CreateElement(key, nameSpaceUri); - element.InnerText = elementInnerText; - metadataElement.AppendChild(element); - } - else { - WriteVerbose(string.Format("Creating XML element failed. Unable to get value from key '{0}'.", key)); - } - } - - requiredModules = ParseRequiredModules(parsedMetadataHash); - if (requiredModules != null) - { - XmlElement dependenciesElement = doc.CreateElement("dependencies", nameSpaceUri); - - foreach (string dependencyName in requiredModules.Keys) - { - XmlElement element = doc.CreateElement("dependency", nameSpaceUri); - - element.SetAttribute("id", dependencyName); - string dependencyVersion = requiredModules[dependencyName].ToString(); - if (!string.IsNullOrEmpty(dependencyVersion)) - { - element.SetAttribute("version", requiredModules[dependencyName].ToString()); - } - - dependenciesElement.AppendChild(element); - } - metadataElement.AppendChild(dependenciesElement); - } - - packageElement.AppendChild(metadataElement); - doc.AppendChild(packageElement); - - var nuspecFullName = System.IO.Path.Combine(outputDir, _pkgName + ".nuspec"); - doc.Save(nuspecFullName); - - WriteVerbose("The newly created nuspec is: " + nuspecFullName); - - return nuspecFullName; - } - - private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash) - { - WriteDebug("In PublishPSResource::ParseRequiredModules()"); - if (!parsedMetadataHash.ContainsKey("requiredmodules")) - { - return null; - } - LanguagePrimitives.TryConvertTo(parsedMetadataHash["requiredmodules"], out object[] requiredModules); - - // Required modules can be: - // a. An array of hash tables of module name and version - // b. A single hash table of module name and version - // c. A string array of module names - // d. A single string module name - - var dependenciesHash = new Hashtable(); - foreach (var reqModule in requiredModules) - { - if (LanguagePrimitives.TryConvertTo(reqModule, out Hashtable moduleHash)) - { - string moduleName = moduleHash["ModuleName"] as string; - - if (moduleHash.ContainsKey("ModuleVersion")) - { - dependenciesHash.Add(moduleName, moduleHash["ModuleVersion"]); - } - else if (moduleHash.ContainsKey("RequiredVersion")) - { - dependenciesHash.Add(moduleName, moduleHash["RequiredVersion"]); - } - else { - dependenciesHash.Add(moduleName, string.Empty); - } - } - else if (LanguagePrimitives.TryConvertTo(reqModule, out string moduleName)) - { - dependenciesHash.Add(moduleName, string.Empty); - } - } - var externalModuleDeps = parsedMetadataHash.ContainsKey("ExternalModuleDependencies") ? - parsedMetadataHash["ExternalModuleDependencies"] : null; - - if (externalModuleDeps != null && LanguagePrimitives.TryConvertTo(externalModuleDeps, out string[] externalModuleNames)) - { - foreach (var extModName in externalModuleNames) - { - if (dependenciesHash.ContainsKey(extModName)) - { - dependenciesHash.Remove(extModName); - } - } - } - - return dependenciesHash; - } - - private bool CheckDependenciesExist(Hashtable dependencies, string repositoryName) - { - WriteDebug("In PublishPSResource::CheckDependenciesExist()"); - // Check to see that all dependencies are in the repository - // Searches for each dependency in the repository the pkg is being pushed to, - // If the dependency is not there, error - foreach (DictionaryEntry dependency in dependencies) + if (!_isNupkgPathSpecified) { - // Need to make individual calls since we're look for exact version numbers or ranges. - var depName = dependency.Key as string; - // test version - string depVersion = dependencies[depName] as string; - depVersion = string.IsNullOrWhiteSpace(depVersion) ? "*" : depVersion; - - if (!Utils.TryGetVersionType( - version: depVersion, - nugetVersion: out NuGetVersion nugetVersion, - versionRange: out VersionRange versionRange, - versionType: out VersionType versionType, - error: out string error)) - { - ThrowTerminatingError(new ErrorRecord( - new ArgumentException(error), - "IncorrectVersionFormat", - ErrorCategory.InvalidArgument, - this)); - } - - // Search for and return the dependency if it's in the repository. - FindHelper findHelper = new FindHelper(_cancellationToken, this, _networkCredential); - - var repository = new[] { repositoryName }; - // Note: we set prerelease argument for FindByResourceName() to true because if no version is specified we want latest version (including prerelease). - // If version is specified it will get that one. There is also no way to specify a prerelease flag with RequiredModules hashtable of dependency so always try to get latest version. - var dependencyFound = findHelper.FindByResourceName(new string[] { depName }, ResourceType.Module, versionRange, nugetVersion, versionType, depVersion, prerelease: true, tag: null, repository, includeDependencies: false, suppressErrors: true); - if (dependencyFound == null || !dependencyFound.Any()) - { - WriteError(new ErrorRecord( - new ArgumentException($"Dependency '{depName}' was not found in repository '{repositoryName}'. Make sure the dependency is published to the repository before publishing this module."), - "DependencyNotFound", - ErrorCategory.ObjectNotFound, - this)); - - return false; - } + _publishHelper.PackResource(); } - return true; - } - - private bool PackNupkg(string outputDir, string outputNupkgDir, string nuspecFile, out ErrorRecord error) - { - WriteDebug("In PublishPSResource::PackNupkg()"); - // Pack the module or script into a nupkg given a nuspec. - var builder = new PackageBuilder(); - try - { - var runner = new PackCommandRunner( - new PackArgs - { - CurrentDirectory = outputDir, - OutputDirectory = outputNupkgDir, - Path = nuspecFile, - Exclude = System.Array.Empty(), - Symbols = false, - Logger = NullLogger.Instance - }, - MSBuildProjectFactory.ProjectCreator, - builder); - bool success = runner.RunPackageBuild(); - - if (success) - { - WriteVerbose("Successfully packed the resource into a .nupkg"); - } - else - { - error = new ErrorRecord( - new InvalidOperationException("Not able to successfully pack the resource into a .nupkg"), - "failedToPackIntoNupkg", - ErrorCategory.ObjectNotFound, - this); - return false; - } - } - catch (Exception e) - { - error = new ErrorRecord( - new ArgumentException($"Unexpected error packing into .nupkg: '{e.Message}'."), - "ErrorPackingIntoNupkg", - ErrorCategory.NotSpecified, - this); - - // exit process record - return false; - } - - error = null; - return true; - } - - private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, out ErrorRecord error) - { - WriteDebug("In PublishPSResource::PushNupkg()"); - // Push the nupkg to the appropriate repository - // Pkg version is parsed from .ps1 file or .psd1 file - var fullNupkgFile = System.IO.Path.Combine(outputNupkgDir, _pkgName + "." + _pkgVersion.ToNormalizedString() + ".nupkg"); - - // The PSGallery uses the v2 protocol still and publishes to a slightly different endpoint: - // "https://www.powershellgallery.com/api/v2/package" - // Until the PSGallery is moved onto the NuGet v3 server protocol, we'll modify the repository uri - // to accommodate for the approprate publish location. - string publishLocation = repoUri.EndsWith("/v2", StringComparison.OrdinalIgnoreCase) ? repoUri + "/package" : repoUri; - - var settings = NuGet.Configuration.Settings.LoadDefaultSettings(null, null, null); - var success = false; - - var sourceProvider = new PackageSourceProvider(settings); - if (Credential != null || _networkCredential != null) - { - InjectCredentialsToSettings(settings, sourceProvider, publishLocation); - } - - - try - { - PushRunner.Run( - settings: Settings.LoadDefaultSettings(root: null, configFileName: null, machineWideSettings: null), - sourceProvider: sourceProvider, - packagePaths: new List { fullNupkgFile }, - source: publishLocation, - apiKey: ApiKey, - symbolSource: null, - symbolApiKey: null, - timeoutSeconds: 0, - disableBuffering: false, - noSymbols: false, - noServiceEndpoint: false, // enable server endpoint - skipDuplicate: false, // if true-- if a package and version already exists, skip it and continue with the next package in the push, if any. - logger: NullLogger.Instance // nuget logger - ).GetAwaiter().GetResult(); - } - catch (HttpRequestException e) - { - WriteVerbose(string.Format("Not able to publish resource to '{0}'", repoUri)); - // look in PS repo for how httpRequestExceptions are handled - - // Unfortunately there is no response message are no status codes provided with the exception and no - var ex = new ArgumentException(String.Format("Repository '{0}': {1}", repoName, e.Message)); - if (e.Message.Contains("400")) - { - if (e.Message.Contains("Api")) - { - // For ADO repositories, public and private, when ApiKey is not provided. - error = new ErrorRecord( - new ArgumentException($"Repository '{repoName}': Please try running again with the -ApiKey parameter and specific API key for the repository specified. For Azure Devops repository, set this to an arbitrary value, for example '-ApiKey AzureDevOps'"), - "400ApiKeyError", - ErrorCategory.AuthenticationError, - this); - } - else - { - error = new ErrorRecord( - ex, - "400Error", - ErrorCategory.PermissionDenied, - this); - } - } - else if (e.Message.Contains("401")) - { - if (e.Message.Contains("API")) - { - // For PSGallery when ApiKey is not provided. - error = new ErrorRecord( - new ArgumentException($"Could not publish to repository '{repoName}'. Please try running again with the -ApiKey parameter and the API key for the repository specified. Exception: '{e.Message}'"), - "401ApiKeyError", - ErrorCategory.AuthenticationError, - this); - } - else - { - // For ADO repository feeds that are public feeds, when the credentials are incorrect. - error = new ErrorRecord(new ArgumentException($"Could not publish to repository '{repoName}'. The Credential provided was incorrect. Exception: '{e.Message}'"), - "401Error", - ErrorCategory.PermissionDenied, - this); ; - } - } - else if (e.Message.Contains("403")) - { - if (repoUri.Contains("myget.org")) - { - // For myGet.org repository feeds when the ApiKey is missing or incorrect. - error = new ErrorRecord( - new ArgumentException($"Could not publish to repository '{repoName}'. The ApiKey provided is incorrect or missing. Please try running again with the -ApiKey parameter and correct API key value for the repository. Exception: '{e.Message}'"), - "403Error", - ErrorCategory.PermissionDenied, - this); - } - else if (repoUri.Contains(".jfrog.io")) - { - // For JFrog Artifactory repository feeds when the ApiKey is provided, whether correct or incorrect, as JFrog does not require -ApiKey (but does require ApiKey to be present as password to -Credential). - error = new ErrorRecord( - new ArgumentException($"Could not publish to repository '{repoName}'. The ApiKey provided is not needed for JFrog Artifactory. Please try running again without the -ApiKey parameter but ensure that -Credential is provided with ApiKey as password. Exception: '{e.Message}'"), - "403Error", - ErrorCategory.PermissionDenied, - this); - } - else - { - error = new ErrorRecord( - ex, - "403Error", - ErrorCategory.PermissionDenied, - this); - } - } - else if (e.Message.Contains("409")) - { - error = new ErrorRecord( - ex, - "409Error", - ErrorCategory.PermissionDenied, this); - } - else - { - error = new ErrorRecord( - ex, - "HTTPRequestError", - ErrorCategory.PermissionDenied, - this); - } - - return success; - } - catch (NuGet.Protocol.Core.Types.FatalProtocolException e) - { - // for ADO repository feeds that are private feeds the error thrown is different and the 401 is in the inner exception message - if (e.InnerException.Message.Contains("401")) - { - error = new ErrorRecord( - new ArgumentException($"Could not publish to repository '{repoName}'. The Credential provided was incorrect. Exception '{e.InnerException.Message}'"), - "401FatalProtocolError", - ErrorCategory.AuthenticationError, - this); - } - else - { - error = new ErrorRecord( - new ArgumentException($"Repository '{repoName}': {e.InnerException.Message}"), - "ProtocolFailError", - ErrorCategory.ProtocolError, - this); - } - - return success; - } - catch (Exception e) - { - WriteVerbose($"Not able to publish resource to '{repoUri}'"); - error = new ErrorRecord( - new ArgumentException(e.Message), - "PushNupkgError", - ErrorCategory.InvalidResult, - this); - - return success; - } - - - WriteVerbose(string.Format("Successfully published the resource to '{0}'", repoUri)); - error = null; - success = true; - - return success; - } - - private void InjectCredentialsToSettings(ISettings settings, IPackageSourceProvider sourceProvider, string source) - { - WriteDebug("In PublishPSResource::InjectCredentialsToSettings()"); - if (Credential == null && _networkCredential == null) + if (_publishHelper.ScriptError || !_publishHelper.ShouldProcess) { return; } - var packageSource = sourceProvider.LoadPackageSources().FirstOrDefault(s => s.Source == source); - if (packageSource != null) - { - if (!packageSource.IsEnabled) - { - packageSource.IsEnabled = true; - } - } - - - var networkCred = Credential == null ? _networkCredential : Credential.GetNetworkCredential(); - string key; - - if (packageSource == null) - - { - key = "_" + Guid.NewGuid().ToString().Replace("-", ""); - settings.AddOrUpdate( - ConfigurationConstants.PackageSources, - new SourceItem(key, source)); - } - else - { - key = packageSource.Name; - } - - settings.AddOrUpdate( - ConfigurationConstants.CredentialsSectionName, - new CredentialsItem( - key, - networkCred.UserName, - networkCred.Password, - isPasswordClearText: true, - String.Empty)); + _publishHelper.PushResource(Repository, SkipDependenciesCheck, _networkCredential); } #endregion + } } diff --git a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 new file mode 100644 index 000000000..cdccee7f6 --- /dev/null +++ b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 @@ -0,0 +1,179 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$modPath = "$psscriptroot/../PSGetTestUtils.psm1" +Import-Module $modPath -Force -Verbose + +$testDir = (get-item $psscriptroot).parent.FullName + +function CreateTestModule +{ + param ( + [string] $Path = "$TestDrive", + [string] $ModuleName = 'TestModule' + ) + + $modulePath = Join-Path -Path $Path -ChildPath $ModuleName + $moduleMan = Join-Path $modulePath -ChildPath ($ModuleName + '.psd1') + $moduleSrc = Join-Path $modulePath -ChildPath ($ModuleName + '.psm1') + + if ( Test-Path -Path $modulePath) { + Remove-Item -Path $modulePath -Recurse -Force + } + + $null = New-Item -Path $modulePath -ItemType Directory -Force + + @' + @{{ + RootModule = "{0}.psm1" + ModuleVersion = '1.0.0' + Author = 'None' + Description = 'None' + GUID = '0c2829fc-b165-4d72-9038-ae3a71a755c1' + FunctionsToExport = @('Test1') + RequiredModules = @('NonExistentModule') + }} +'@ -f $ModuleName | Out-File -FilePath $moduleMan + + @' + function Test1 { + Write-Output 'Hello from Test1' + } +'@ | Out-File -FilePath $moduleSrc +} + +Describe "Test Compress-PSResource" -tags 'CI' { + BeforeAll { + Get-NewPSResourceRepositoryFile + + # Register temporary repositories + $tmpRepoPath = Join-Path -Path $TestDrive -ChildPath "tmpRepoPath" + New-Item $tmpRepoPath -Itemtype directory -Force + $testRepository = "testRepository" + Register-PSResourceRepository -Name $testRepository -Uri $tmpRepoPath -Priority 1 -ErrorAction SilentlyContinue + $script:repositoryPath = [IO.Path]::GetFullPath((get-psresourcerepository "testRepository").Uri.AbsolutePath) + + $tmpRepoPath2 = Join-Path -Path $TestDrive -ChildPath "tmpRepoPath2" + New-Item $tmpRepoPath2 -Itemtype directory -Force + $testRepository2 = "testRepository2" + Register-PSResourceRepository -Name $testRepository2 -Uri $tmpRepoPath2 -ErrorAction SilentlyContinue + $script:repositoryPath2 = [IO.Path]::GetFullPath((get-psresourcerepository "testRepository2").Uri.AbsolutePath) + + # Create module + $script:tmpModulesPath = Join-Path -Path $TestDrive -ChildPath "tmpModulesPath" + $script:PublishModuleName = "PSGetTestModule" + $script:PublishModuleBase = Join-Path $script:tmpModulesPath -ChildPath $script:PublishModuleName + if(!(Test-Path $script:PublishModuleBase)) + { + New-Item -Path $script:PublishModuleBase -ItemType Directory -Force + } + $script:PublishModuleBaseUNC = $script:PublishModuleBase -Replace '^(.):', '\\localhost\$1$' + + #Create dependency module + $script:DependencyModuleName = "PackageManagement" + $script:DependencyModuleBase = Join-Path $script:tmpModulesPath -ChildPath $script:DependencyModuleName + if(!(Test-Path $script:DependencyModuleBase)) + { + New-Item -Path $script:DependencyModuleBase -ItemType Directory -Force + } + + # Create temp destination path + $script:destinationPath = [IO.Path]::GetFullPath((Join-Path -Path $TestDrive -ChildPath "tmpDestinationPath")) + New-Item $script:destinationPath -ItemType directory -Force + + #Create folder where we shall place all script files to be published for these tests + $script:tmpScriptsFolderPath = Join-Path -Path $TestDrive -ChildPath "tmpScriptsPath" + if(!(Test-Path $script:tmpScriptsFolderPath)) + { + New-Item -Path $script:tmpScriptsFolderPath -ItemType Directory -Force + } + + # Path to folder, within our test folder, where we store invalid module and script files used for testing + $script:testFilesFolderPath = Join-Path $testDir -ChildPath "testFiles" + + # Path to specifically to that invalid test modules folder + $script:testModulesFolderPath = Join-Path $script:testFilesFolderPath -ChildPath "testModules" + + # Path to specifically to that invalid test scripts folder + $script:testScriptsFolderPath = Join-Path $script:testFilesFolderPath -ChildPath "testScripts" + + # Create test module with missing required module + CreateTestModule -Path $TestDrive -ModuleName 'ModuleWithMissingRequiredModule' + } + AfterAll { + Get-RevertPSResourceRepositoryFile + } + AfterEach { + # Delete all contents of the repository without deleting the repository directory itself + $pkgsToDelete = Join-Path -Path "$script:repositoryPath" -ChildPath "*" + Remove-Item $pkgsToDelete -Recurse + + $pkgsToDelete = Join-Path -Path "$script:repositoryPath2" -ChildPath "*" + Remove-Item $pkgsToDelete -Recurse + + $pkgsToDelete = Join-Path -Path $script:PublishModuleBase -ChildPath "*" + Remove-Item $pkgsToDelete -Recurse -ErrorAction SilentlyContinue + } + + It "Compress-PSResource compresses a module into a nupkg and saves it to the DestinationPath" { + $version = "1.0.0" + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + + Compress-PSResource -Path $script:PublishModuleBase -DestinationPath $script:repositoryPath + + $expectedPath = Join-Path -Path $script:repositoryPath -ChildPath "$script:PublishModuleName.$version.nupkg" + (Get-ChildItem $script:repositoryPath).FullName | Should -Be $expectedPath + } + + It "Compress a module using -Path positional parameter and -Destination positional parameter" { + $version = "1.0.0" + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + + Compress-PSResource $script:PublishModuleBase $script:repositoryPath + + $expectedPath = Join-Path -Path $script:repositoryPath -ChildPath "$script:PublishModuleName.$version.nupkg" + (Get-ChildItem $script:repositoryPath).FullName | Should -Be $expectedPath + } + + It "Compress-PSResource compresses a module and preserves file structure" { + $version = "1.0.0" + $testFile = Join-Path -Path "TestSubDirectory" -ChildPath "TestSubDirFile.ps1" + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + New-Item -Path (Join-Path -Path $script:PublishModuleBase -ChildPath $testFile) -Force + + Compress-PSResource -Path $script:PublishModuleBase -DestinationPath $script:repositoryPath + + # Must change .nupkg to .zip so that Expand-Archive can work on Windows PowerShell + $nupkgPath = Join-Path -Path $script:repositoryPath -ChildPath "$script:PublishModuleName.$version.nupkg" + $zipPath = Join-Path -Path $script:repositoryPath -ChildPath "$script:PublishModuleName.$version.zip" + Rename-Item -Path $nupkgPath -NewName $zipPath + $unzippedPath = Join-Path -Path $TestDrive -ChildPath "$script:PublishModuleName" + New-Item $unzippedPath -Itemtype directory -Force + Expand-Archive -Path $zipPath -DestinationPath $unzippedPath + + Test-Path -Path (Join-Path -Path $unzippedPath -ChildPath $testFile) | Should -Be $True + } +<# Test for Signing the nupkg. Signing doesn't work + It "Compressed Module is able to be signed with a certificate" { + $version = "1.0.0" + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + + Compress-PSResource -Path $script:PublishModuleBase -DestinationPath $script:repositoryPath2 + + $expectedPath = Join-Path -Path $script:repositoryPath2 -ChildPath "$script:PublishModuleName.$version.nupkg" + (Get-ChildItem $script:repositoryPath2).FullName | Should -Be $expectedPath + + # create test cert + # Create a self-signed certificate for code signing + $testCert = New-SelfSignedCertificate -Subject "CN=NuGet Test Developer, OU=Use for testing purposes ONLY" -FriendlyName "NuGetTestDeveloper" -Type CodeSigning -KeyUsage DigitalSignature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -CertStoreLocation "Cert:\CurrentUser\My" + + # sign the nupkg + $nupkgPath = Join-Path -Path $script:repositoryPath2 -ChildPath "$script:PublishModuleName.$version.nupkg" + Set-AuthenticodeSignature -FilePath $nupkgPath -Certificate $testCert + + # Verify the file was signed + $signature = Get-AuthenticodeSignature -FilePath $nupkgPath + $signature.Status | Should -Be 'Valid' + } + #> +} diff --git a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 index 0decd5011..1b2a70d84 100644 --- a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 @@ -290,7 +290,6 @@ Describe "Test Publish-PSResource" -tags 'CI' { {Publish-PSResource -Path $script:PublishModuleBase -ErrorAction Stop} | Should -Throw -ErrorId "DependencyNotFound,Microsoft.PowerShell.PSResourceGet.Cmdlets.PublishPSResource" } - It "Publish a module with -SkipDependenciesCheck" { $version = "1.0.0" $dependencyVersion = "2.0.0" @@ -321,8 +320,23 @@ Describe "Test Publish-PSResource" -tags 'CI' { Test-Path -Path (Join-Path -Path $unzippedPath -ChildPath $testFile) | Should -Be $True } + It "Publish a module with -NupkgPath" { + $version = "1.0.0" + # Make a nupkg + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + Compress-PSResource -Path $script:PublishModuleBase -DestinationPath $script:destinationPath + $expectedPath = Join-Path -Path $script:destinationPath -ChildPath "$script:PublishModuleName.$version.nupkg" + (Get-ChildItem $script:destinationPath).FullName | Should -Be $expectedPath + + # Pass the nupkg via -NupkgPath + Publish-PSResource -NupkgPath $expectedPath -Repository $testRepository2 + $expectedPath = Join-Path -Path $script:repositoryPath2 -ChildPath "$script:PublishModuleName.$version.nupkg" + (Get-ChildItem $script:repositoryPath2).FullName | Should -Be $expectedPath + } + <# The following tests are related to passing in parameters to customize a nuspec. # These parameters are not going in the current release, but is open for discussion to include in the future. + It "Publish a module with -Nuspec" { $version = "1.0.0" New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" -NestedModules "$script:PublishModuleName.psm1"