Skip to content

Commit

Permalink
Add support for publishing .nupkg file to ACR (#1763)
Browse files Browse the repository at this point in the history
  • Loading branch information
anamnavi committed Jan 8, 2025
1 parent a4e5322 commit 25b5edd
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 18 deletions.
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);
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))
{
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

0 comments on commit 25b5edd

Please sign in to comment.