diff --git a/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs b/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs index 246e91a8cc..1543623a12 100644 --- a/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs +++ b/src/Agent.Plugins/Artifact/PipelineArtifactConstants.cs @@ -16,5 +16,6 @@ public class PipelineArtifactConstants public const string RootId = "RootId"; public const string SaveCache = "SaveCache"; public const string FileShareArtifact = "filepath"; + public const string CustomPropertiesPrefix = "user-"; } } \ No newline at end of file diff --git a/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs b/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs index ae84a9cc44..459c9d56a1 100644 --- a/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs +++ b/src/Agent.Plugins/Artifact/PipelineArtifactServer.cs @@ -37,6 +37,7 @@ internal async Task UploadAsync( int pipelineId, string name, string source, + IDictionary properties, CancellationToken cancellationToken) { VssConnection connection = context.VssConnection; @@ -69,10 +70,13 @@ internal async Task UploadAsync( // 2) associate the pipeline artifact with an build artifact BuildServer buildServer = new BuildServer(connection); - Dictionary propertiesDictionary = new Dictionary(); - propertiesDictionary.Add(PipelineArtifactConstants.RootId, result.RootId.ValueString); - propertiesDictionary.Add(PipelineArtifactConstants.ProofNodes, StringUtil.ConvertToJson(result.ProofNodes.ToArray())); - propertiesDictionary.Add(PipelineArtifactConstants.ArtifactSize, result.ContentSize.ToString()); + + var propertiesDictionary = new Dictionary(properties ?? new Dictionary()) + { + { PipelineArtifactConstants.RootId, result.RootId.ValueString }, + { PipelineArtifactConstants.ProofNodes, StringUtil.ConvertToJson(result.ProofNodes.ToArray()) }, + { PipelineArtifactConstants.ArtifactSize, result.ContentSize.ToString() } + }; BuildArtifact buildArtifact = await AsyncHttpRetryHelper.InvokeAsync( async () => diff --git a/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPlugin.cs b/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPlugin.cs index aa1af669cb..6781325275 100644 --- a/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPlugin.cs +++ b/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPlugin.cs @@ -12,6 +12,8 @@ using Agent.Sdk; using System.Text.RegularExpressions; using Microsoft.VisualStudio.Services.Content.Common.Tracing; +using System.Linq; +using System.Text.Json; namespace Agent.Plugins.PipelineArtifact { @@ -64,6 +66,8 @@ public class PublishPipelineArtifactTask : PipelineArtifactTaskPluginBase // create a normalized identifier-compatible string (A-Z, a-z, 0-9, -, and .) and remove .default since it's redundant public static readonly Regex jobIdentifierRgx = new Regex("[^a-zA-Z0-9 - .]", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private const string customProperties = "properties"; + protected override async Task ProcessCommandInternalAsync( AgentTaskPluginExecutionContext context, string targetPath, @@ -105,12 +109,38 @@ protected override async Task ProcessCommandInternalAsync( throw new FileNotFoundException(StringUtil.Loc("PathDoesNotExist", targetPath)); } + string propertiesStr = context.GetInput(customProperties, required: false); + IDictionary properties = ParseCustomProperties(propertiesStr); + // Upload to VSTS BlobStore, and associate the artifact with the build. context.Output(StringUtil.Loc("UploadingPipelineArtifact", fullPath, buildId)); PipelineArtifactServer server = new PipelineArtifactServer(tracer); - await server.UploadAsync(context, projectId, buildId, artifactName, fullPath, token); + await server.UploadAsync(context, projectId, buildId, artifactName, fullPath, properties, token); context.Output(StringUtil.Loc("UploadArtifactFinished")); } + private IDictionary ParseCustomProperties(string properties) + { + if (string.IsNullOrWhiteSpace(properties)) + { + return null; + } + + try + { + var propertyBag = StringUtil.ConvertFromJson>(properties); + var prefixMissing = propertyBag.Keys.FirstOrDefault(k => !k.StartsWith(PipelineArtifactConstants.CustomPropertiesPrefix)); + if (!string.IsNullOrWhiteSpace(prefixMissing)) + { + throw new InvalidOperationException(StringUtil.Loc("ArtifactCustomPropertyInvalid", prefixMissing)); + } + + return propertyBag; + } + catch (JsonException) + { + throw new ArgumentException(StringUtil.Loc("ArtifactCustomPropertiesNotJson", properties)); + } + } private string NormalizeJobIdentifier(string jobIdentifier) { diff --git a/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPluginV1.cs b/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPluginV1.cs index e5ef7af0ef..6ea3e51906 100644 --- a/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPluginV1.cs +++ b/src/Agent.Plugins/PipelineArtifact/PipelineArtifactPluginV1.cs @@ -16,6 +16,7 @@ using System.Text.RegularExpressions; using System.Runtime.InteropServices; using Microsoft.VisualStudio.Services.Content.Common.Tracing; +using Newtonsoft.Json; namespace Agent.Plugins.PipelineArtifact { @@ -63,6 +64,7 @@ public class PublishPipelineArtifactTaskV1 : PipelineArtifactTaskPluginBaseV1 private static readonly Regex jobIdentifierRgx = new Regex("[^a-zA-Z0-9 - .]", RegexOptions.Compiled | RegexOptions.CultureInvariant); private const string pipelineType = "pipeline"; private const string fileShareType = "filepath"; + private const string customProperties = "properties"; protected override async Task ProcessCommandInternalAsync( AgentTaskPluginExecutionContext context, @@ -83,6 +85,9 @@ protected override async Task ProcessCommandInternalAsync( string defaultWorkingDirectory = context.Variables.GetValueOrDefault("system.defaultworkingdirectory").Value; + string propertiesStr = context.GetInput(customProperties, required: false); + IDictionary properties = ParseCustomProperties(propertiesStr); + bool onPrem = !String.Equals(context.Variables.GetValueOrDefault(WellKnownDistributedTaskVariables.ServerType)?.Value, "Hosted", StringComparison.OrdinalIgnoreCase); if (onPrem) { @@ -138,7 +143,7 @@ protected override async Task ProcessCommandInternalAsync( // Upload to VSTS BlobStore, and associate the artifact with the build. context.Output(StringUtil.Loc("UploadingPipelineArtifact", fullPath, buildId)); PipelineArtifactServer server = new PipelineArtifactServer(tracer); - await server.UploadAsync(context, projectId, buildId, artifactName, fullPath, token); + await server.UploadAsync(context, projectId, buildId, artifactName, fullPath, properties, token); context.Output(StringUtil.Loc("UploadArtifactFinished")); } @@ -161,6 +166,30 @@ protected override async Task ProcessCommandInternalAsync( } } + private IDictionary ParseCustomProperties(string properties) + { + if (string.IsNullOrWhiteSpace(properties)) + { + return null; + } + + try + { + var propertyBag = StringUtil.ConvertFromJson>(properties); + var prefixMissing = propertyBag.Keys.FirstOrDefault(k => !k.StartsWith(PipelineArtifactConstants.CustomPropertiesPrefix)); + if (!string.IsNullOrWhiteSpace(prefixMissing)) + { + throw new InvalidOperationException(StringUtil.Loc("ArtifactCustomPropertyInvalid", prefixMissing)); + } + + return propertyBag; + } + catch (JsonException) + { + throw new ArgumentException(StringUtil.Loc("ArtifactCustomPropertiesNotJson", properties)); + } + } + private string NormalizeJobIdentifier(string jobIdentifier) { jobIdentifier = jobIdentifierRgx.Replace(jobIdentifier, string.Empty).Replace(".default", string.Empty); diff --git a/src/Misc/layoutbin/en-US/strings.json b/src/Misc/layoutbin/en-US/strings.json index 4e741d0b94..15f319e8e8 100644 --- a/src/Misc/layoutbin/en-US/strings.json +++ b/src/Misc/layoutbin/en-US/strings.json @@ -26,6 +26,8 @@ "AllowContainerUserRunDocker": "Allow user '{0}' run any docker command without SUDO.", "AlreadyConfiguredError": "Cannot configure the agent because it is already configured. To reconfigure the agent, run 'config.cmd remove' or './config.sh remove' first.", "ArgumentNeeded": "'{0}' has to be specified.", + "ArtifactCustomPropertiesNotJson": "Artifact custom properties is not valid JSON: '{0}'", + "ArtifactCustomPropertyInvalid": "Artifact custom properties must be prefixed with 'user-'. Invalid property: '{0}'", "ArtifactDownloadFailed": "Failed to download the artifact from {0}.", "ArtifactLocationRequired": "Artifact location is required.", "ArtifactNameIsNotValid": "Artifact name is not valid: {0}. It cannot contain '\\', /', \"', ':', '<', '>', '|', '*', and '?'",