Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for publishing .nupkg file to ACR #1763

Merged
merged 18 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/code/ContainerRegistryServerAPICalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -649,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
Expand Down Expand Up @@ -1115,23 +1115,26 @@ private static Collection<KeyValuePair<string, string>> GetDefaultHeaders(string
#endregion

#region Publish Methods

/// <summary>
/// Helper method that publishes a package to the container registry.
/// This gets called from Publish-PSResource.
/// </summary>
internal bool PushNupkgContainerRegistry(string psd1OrPs1File,
internal bool PushNupkgContainerRegistry(
string outputNupkgDir,
string packageName,
string modulePrefix,
NuGetVersion packageVersion,
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();
Expand Down
19 changes: 10 additions & 9 deletions src/code/PSResourceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,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);
Expand Down Expand Up @@ -883,25 +884,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)
Expand Down Expand Up @@ -937,7 +938,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();

Expand All @@ -948,19 +949,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();
}
Expand Down
212 changes: 208 additions & 4 deletions src/code/PublishHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -440,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);
anamnavi marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Expand All @@ -455,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))
{
Expand All @@ -474,7 +492,8 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe
}
finally
{
if (!_isNupkgPathSpecified)
// For scenarios such as Publish-PSResource -NupkgPath -Repository <non-container registry 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);
Expand Down Expand Up @@ -1243,6 +1262,191 @@ private bool CheckDependenciesExist(Hashtable dependencies, string repositoryNam
return true;
}

/// <summary>
/// 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
/// </summary>
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;
}

/// <summary>
/// Get package info from the .nupkg file provided, inluding package name (_pkgName), package version (_pkgVersion), and metadata parsed into a hashtable (parsedMetadata)
/// </summary>
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))
{
anamnavi marked this conversation as resolved.
Show resolved Hide resolved
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);
}

/// <summary>
/// Extract copied .nupkg, find metadata file (either .ps1, .psd1, or .nuspec) and read metadata into a hashtable
/// </summary>
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<string> pkgTags = new List<string>();

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
}
}
Loading
Loading