mirror of
https://github.com/actions/runner.git
synced 2025-12-16 06:57:25 +00:00
GitHub Actions Runner
This commit is contained in:
61
src/Sdk/DTPipelines/Pipelines/ActionStep.cs
Normal file
61
src/Sdk/DTPipelines/Pipelines/ActionStep.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class ActionStep : JobStep
|
||||
{
|
||||
[JsonConstructor]
|
||||
public ActionStep()
|
||||
{
|
||||
}
|
||||
|
||||
private ActionStep(ActionStep actionToClone)
|
||||
: base(actionToClone)
|
||||
{
|
||||
this.Reference = actionToClone.Reference?.Clone();
|
||||
|
||||
Environment = actionToClone.Environment?.Clone();
|
||||
Inputs = actionToClone.Inputs?.Clone();
|
||||
ContextName = actionToClone?.ContextName;
|
||||
ScopeName = actionToClone?.ScopeName;
|
||||
DisplayNameToken = actionToClone.DisplayNameToken?.Clone();
|
||||
}
|
||||
|
||||
public override StepType Type => StepType.Action;
|
||||
|
||||
[DataMember]
|
||||
public ActionStepDefinitionReference Reference
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
// TODO: After TFS and legacy phases/steps/ect are removed, lets replace the DisplayName in the base class with this value and remove this additional prop
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken DisplayNameToken { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String ScopeName { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String ContextName { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken Environment { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken Inputs { get; set; }
|
||||
|
||||
public override Step Clone()
|
||||
{
|
||||
return new ActionStep(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/Sdk/DTPipelines/Pipelines/ActionStepDefinitionReference.cs
Normal file
153
src/Sdk/DTPipelines/Pipelines/ActionStepDefinitionReference.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public enum ActionSourceType
|
||||
{
|
||||
[DataMember]
|
||||
Repository = 1,
|
||||
|
||||
[DataMember]
|
||||
ContainerRegistry = 2,
|
||||
|
||||
[DataMember]
|
||||
Script = 3
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
[KnownType(typeof(ContainerRegistryReference))]
|
||||
[KnownType(typeof(RepositoryPathReference))]
|
||||
[KnownType(typeof(ScriptReference))]
|
||||
[JsonConverter(typeof(ActionStepDefinitionReferenceConverter))]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public abstract class ActionStepDefinitionReference
|
||||
{
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public abstract ActionSourceType Type { get; }
|
||||
|
||||
public abstract ActionStepDefinitionReference Clone();
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class ContainerRegistryReference : ActionStepDefinitionReference
|
||||
{
|
||||
[JsonConstructor]
|
||||
public ContainerRegistryReference()
|
||||
{
|
||||
}
|
||||
|
||||
private ContainerRegistryReference(ContainerRegistryReference referenceToClone)
|
||||
{
|
||||
this.Image = referenceToClone.Image;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public override ActionSourceType Type => ActionSourceType.ContainerRegistry;
|
||||
|
||||
/// <summary>
|
||||
/// Container image
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string Image
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override ActionStepDefinitionReference Clone()
|
||||
{
|
||||
return new ContainerRegistryReference(this);
|
||||
}
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class RepositoryPathReference : ActionStepDefinitionReference
|
||||
{
|
||||
[JsonConstructor]
|
||||
public RepositoryPathReference()
|
||||
{
|
||||
}
|
||||
|
||||
private RepositoryPathReference(RepositoryPathReference referenceToClone)
|
||||
{
|
||||
this.Name = referenceToClone.Name;
|
||||
this.Ref = referenceToClone.Ref;
|
||||
this.RepositoryType = referenceToClone.RepositoryType;
|
||||
this.Path = referenceToClone.Path;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public override ActionSourceType Type => ActionSourceType.Repository;
|
||||
|
||||
/// <summary>
|
||||
/// Repository name
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repository ref, branch/tag/commit
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string Ref
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repository type, github/AzureRepo/etc
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string RepositoryType
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to action entry point directory
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string Path
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override ActionStepDefinitionReference Clone()
|
||||
{
|
||||
return new RepositoryPathReference(this);
|
||||
}
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class ScriptReference : ActionStepDefinitionReference
|
||||
{
|
||||
[JsonConstructor]
|
||||
public ScriptReference()
|
||||
{
|
||||
}
|
||||
|
||||
private ScriptReference(ScriptReference referenceToClone)
|
||||
{
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public override ActionSourceType Type => ActionSourceType.Script;
|
||||
|
||||
public override ActionStepDefinitionReference Clone()
|
||||
{
|
||||
return new ScriptReference(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
internal sealed class ActionStepDefinitionReferenceConverter : VssSecureJsonConverter
|
||||
{
|
||||
public override bool CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(Step).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
Object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.StartObject)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JObject value = JObject.Load(reader);
|
||||
if (value.TryGetValue("Type", StringComparison.OrdinalIgnoreCase, out JToken actionTypeValue))
|
||||
{
|
||||
ActionSourceType actionType;
|
||||
if (actionTypeValue.Type == JTokenType.Integer)
|
||||
{
|
||||
actionType = (ActionSourceType)(Int32)actionTypeValue;
|
||||
}
|
||||
else if (actionTypeValue.Type != JTokenType.String || !Enum.TryParse((String)actionTypeValue, true, out actionType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ActionStepDefinitionReference reference = null;
|
||||
switch (actionType)
|
||||
{
|
||||
case ActionSourceType.Repository:
|
||||
reference = new RepositoryPathReference();
|
||||
break;
|
||||
|
||||
case ActionSourceType.ContainerRegistry:
|
||||
reference = new ContainerRegistryReference();
|
||||
break;
|
||||
|
||||
case ActionSourceType.Script:
|
||||
reference = new ScriptReference();
|
||||
break;
|
||||
}
|
||||
|
||||
using (var objectReader = value.CreateReader())
|
||||
{
|
||||
serializer.Populate(objectReader, reference);
|
||||
}
|
||||
|
||||
return reference;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
397
src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs
Normal file
397
src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs
Normal file
@@ -0,0 +1,397 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class AgentJobRequestMessage
|
||||
{
|
||||
[JsonConstructor]
|
||||
internal AgentJobRequestMessage()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Job request message sent to the runner
|
||||
/// </summary>
|
||||
/// <param name="environmentVariables">Hierarchy of environment variables to overlay, last wins.</param>
|
||||
public AgentJobRequestMessage(
|
||||
TaskOrchestrationPlanReference plan,
|
||||
TimelineReference timeline,
|
||||
Guid jobId,
|
||||
String jobDisplayName,
|
||||
String jobName,
|
||||
TemplateToken jobContainer,
|
||||
TemplateToken jobServiceContainers,
|
||||
IList<TemplateToken> environmentVariables,
|
||||
IDictionary<String, VariableValue> variables,
|
||||
IList<MaskHint> maskHints,
|
||||
JobResources jobResources,
|
||||
DictionaryContextData contextData,
|
||||
WorkspaceOptions workspaceOptions,
|
||||
IEnumerable<JobStep> steps,
|
||||
IEnumerable<ContextScope> scopes)
|
||||
{
|
||||
this.MessageType = JobRequestMessageTypes.PipelineAgentJobRequest;
|
||||
this.Plan = plan;
|
||||
this.JobId = jobId;
|
||||
this.JobDisplayName = jobDisplayName;
|
||||
this.JobName = jobName;
|
||||
this.JobContainer = jobContainer;
|
||||
this.JobServiceContainers = jobServiceContainers;
|
||||
this.Timeline = timeline;
|
||||
this.Resources = jobResources;
|
||||
this.Workspace = workspaceOptions;
|
||||
|
||||
m_variables = new Dictionary<String, VariableValue>(variables, StringComparer.OrdinalIgnoreCase);
|
||||
m_maskHints = new List<MaskHint>(maskHints);
|
||||
m_steps = new List<JobStep>(steps);
|
||||
|
||||
if (scopes != null)
|
||||
{
|
||||
m_scopes = new List<ContextScope>(scopes);
|
||||
}
|
||||
|
||||
if (environmentVariables?.Count > 0)
|
||||
{
|
||||
m_environmentVariables = new List<TemplateToken>(environmentVariables);
|
||||
}
|
||||
|
||||
this.ContextData = new Dictionary<String, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
||||
if (contextData?.Count > 0)
|
||||
{
|
||||
foreach (var pair in contextData)
|
||||
{
|
||||
this.ContextData[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public String MessageType
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public TaskOrchestrationPlanReference Plan
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public TimelineReference Timeline
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public Guid JobId
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public String JobDisplayName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public String JobName
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken JobContainer
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken JobServiceContainers
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public Int64 RequestId
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public DateTime LockedUntil
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public JobResources Resources
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public IDictionary<String, PipelineContextData> ContextData
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public WorkspaceOptions Workspace
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of mask hints
|
||||
/// </summary>
|
||||
public List<MaskHint> MaskHints
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_maskHints == null)
|
||||
{
|
||||
m_maskHints = new List<MaskHint>();
|
||||
}
|
||||
return m_maskHints;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hierarchy of environment variables to overlay, last wins.
|
||||
/// </summary>
|
||||
public IList<TemplateToken> EnvironmentVariables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_environmentVariables == null)
|
||||
{
|
||||
m_environmentVariables = new List<TemplateToken>();
|
||||
}
|
||||
return m_environmentVariables;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of variables associated with the current context.
|
||||
/// </summary>
|
||||
public IDictionary<String, VariableValue> Variables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_variables == null)
|
||||
{
|
||||
m_variables = new Dictionary<String, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
return m_variables;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<JobStep> Steps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_steps == null)
|
||||
{
|
||||
m_steps = new List<JobStep>();
|
||||
}
|
||||
return m_steps;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<ContextScope> Scopes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_scopes == null)
|
||||
{
|
||||
m_scopes = new List<ContextScope>();
|
||||
}
|
||||
return m_scopes;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
public void SetJobSidecarContainers(IDictionary<String, String> value)
|
||||
{
|
||||
m_jobSidecarContainers = value;
|
||||
}
|
||||
|
||||
public TaskAgentMessage GetAgentMessage()
|
||||
{
|
||||
var body = JsonUtility.ToString(this);
|
||||
|
||||
return new TaskAgentMessage
|
||||
{
|
||||
Body = body,
|
||||
MessageType = JobRequestMessageTypes.PipelineAgentJobRequest
|
||||
};
|
||||
}
|
||||
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
internal static TemplateToken ConvertToTemplateToken(ContainerResource resource)
|
||||
{
|
||||
var result = new MappingToken(null, null, null);
|
||||
|
||||
var image = resource.Image;
|
||||
if (!string.IsNullOrEmpty(image))
|
||||
{
|
||||
result.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, image));
|
||||
}
|
||||
|
||||
var options = resource.Options;
|
||||
if (!string.IsNullOrEmpty(options))
|
||||
{
|
||||
result.Add(new StringToken(null, null, null, "options"), new StringToken(null, null, null, options));
|
||||
}
|
||||
|
||||
var environment = resource.Environment;
|
||||
if (environment?.Count > 0)
|
||||
{
|
||||
var mapping = new MappingToken(null, null, null);
|
||||
foreach (var pair in environment)
|
||||
{
|
||||
mapping.Add(new StringToken(null, null, null, pair.Key), new StringToken(null, null, null, pair.Value));
|
||||
}
|
||||
result.Add(new StringToken(null, null, null, "env"), mapping);
|
||||
}
|
||||
|
||||
var ports = resource.Ports;
|
||||
if (ports?.Count > 0)
|
||||
{
|
||||
var sequence = new SequenceToken(null, null, null);
|
||||
foreach (var item in ports)
|
||||
{
|
||||
sequence.Add(new StringToken(null, null, null, item));
|
||||
}
|
||||
result.Add(new StringToken(null, null, null, "ports"), sequence);
|
||||
}
|
||||
|
||||
var volumes = resource.Volumes;
|
||||
if (volumes?.Count > 0)
|
||||
{
|
||||
var sequence = new SequenceToken(null, null, null);
|
||||
foreach (var item in volumes)
|
||||
{
|
||||
sequence.Add(new StringToken(null, null, null, item));
|
||||
}
|
||||
result.Add(new StringToken(null, null, null, "volumes"), sequence);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
if (JobContainer is StringToken jobContainerStringToken)
|
||||
{
|
||||
var resourceAlias = jobContainerStringToken.Value;
|
||||
var resource = Resources?.Containers.SingleOrDefault(x => string.Equals(x.Alias, resourceAlias, StringComparison.OrdinalIgnoreCase));
|
||||
if (resource != null)
|
||||
{
|
||||
JobContainer = ConvertToTemplateToken(resource);
|
||||
m_jobContainerResourceAlias = resourceAlias;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
if (m_jobSidecarContainers?.Count > 0 && (JobServiceContainers == null || JobServiceContainers.Type == TokenType.Null))
|
||||
{
|
||||
var services = new MappingToken(null, null, null);
|
||||
foreach (var pair in m_jobSidecarContainers)
|
||||
{
|
||||
var networkAlias = pair.Key;
|
||||
var serviceResourceAlias = pair.Value;
|
||||
var serviceResource = Resources.Containers.Single(x => string.Equals(x.Alias, serviceResourceAlias, StringComparison.OrdinalIgnoreCase));
|
||||
services.Add(new StringToken(null, null, null, networkAlias), ConvertToTemplateToken(serviceResource));
|
||||
}
|
||||
JobServiceContainers = services;
|
||||
}
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_environmentVariables?.Count == 0)
|
||||
{
|
||||
m_environmentVariables = null;
|
||||
}
|
||||
|
||||
if (m_maskHints?.Count == 0)
|
||||
{
|
||||
m_maskHints = null;
|
||||
}
|
||||
else if (m_maskHints != null)
|
||||
{
|
||||
m_maskHints = new List<MaskHint>(this.m_maskHints.Distinct());
|
||||
}
|
||||
|
||||
if (m_scopes?.Count == 0)
|
||||
{
|
||||
m_scopes = null;
|
||||
}
|
||||
|
||||
if (m_variables?.Count == 0)
|
||||
{
|
||||
m_variables = null;
|
||||
}
|
||||
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
if (!string.IsNullOrEmpty(m_jobContainerResourceAlias))
|
||||
{
|
||||
JobContainer = new StringToken(null, null, null, m_jobContainerResourceAlias);
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "EnvironmentVariables", EmitDefaultValue = false)]
|
||||
private List<TemplateToken> m_environmentVariables;
|
||||
|
||||
[DataMember(Name = "Mask", EmitDefaultValue = false)]
|
||||
private List<MaskHint> m_maskHints;
|
||||
|
||||
[DataMember(Name = "Steps", EmitDefaultValue = false)]
|
||||
private List<JobStep> m_steps;
|
||||
|
||||
[DataMember(Name = "Scopes", EmitDefaultValue = false)]
|
||||
private List<ContextScope> m_scopes;
|
||||
|
||||
[DataMember(Name = "Variables", EmitDefaultValue = false)]
|
||||
private IDictionary<String, VariableValue> m_variables;
|
||||
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
[DataMember(Name = "JobSidecarContainers", EmitDefaultValue = false)]
|
||||
private IDictionary<String, String> m_jobSidecarContainers;
|
||||
|
||||
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
|
||||
[IgnoreDataMember]
|
||||
private string m_jobContainerResourceAlias;
|
||||
}
|
||||
}
|
||||
769
src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessageUtil.cs
Normal file
769
src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessageUtil.cs
Normal file
@@ -0,0 +1,769 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class AgentJobRequestMessageUtil
|
||||
{
|
||||
// Legacy JobRequestMessage -> Pipeline JobRequestMessage
|
||||
// Used by the agent when the latest version agent connect to old version TFS
|
||||
// Used by the server when common method only take the new Message contact, like, telemetry logging
|
||||
public static AgentJobRequestMessage Convert(WebApi.AgentJobRequestMessage message)
|
||||
{
|
||||
// construct steps
|
||||
List<JobStep> jobSteps = new List<JobStep>();
|
||||
foreach (var task in message.Tasks)
|
||||
{
|
||||
TaskStep taskStep = new TaskStep(task);
|
||||
jobSteps.Add(taskStep);
|
||||
}
|
||||
|
||||
Dictionary<String, VariableValue> variables = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
||||
HashSet<MaskHint> maskHints = new HashSet<MaskHint>();
|
||||
JobResources jobResources = new JobResources();
|
||||
WorkspaceOptions workspace = new WorkspaceOptions();
|
||||
message.Environment.Extract(variables, maskHints, jobResources);
|
||||
|
||||
// convert repository endpoint into checkout task for Build
|
||||
if (string.Equals(message.Plan.PlanType, "Build", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// repositoryId was added sometime after TFS2015, so we need to fall back to find endpoint using endpoint type.
|
||||
var legacyRepoEndpoint = jobResources.Endpoints.FirstOrDefault(x => x.Data.ContainsKey("repositoryId"));
|
||||
if (legacyRepoEndpoint == null)
|
||||
{
|
||||
legacyRepoEndpoint = jobResources.Endpoints.FirstOrDefault(x => x.Type == LegacyRepositoryTypes.Bitbucket || x.Type == LegacyRepositoryTypes.Git || x.Type == LegacyRepositoryTypes.TfsGit || x.Type == LegacyRepositoryTypes.GitHub || x.Type == LegacyRepositoryTypes.GitHubEnterprise || x.Type == LegacyRepositoryTypes.TfsVersionControl);
|
||||
}
|
||||
|
||||
// build retention job will not have a repo endpoint.
|
||||
if (legacyRepoEndpoint != null)
|
||||
{
|
||||
// construct checkout task
|
||||
var checkoutStep = new TaskStep();
|
||||
checkoutStep.Id = Guid.NewGuid();
|
||||
checkoutStep.DisplayName = PipelineConstants.CheckoutTask.FriendlyName;
|
||||
checkoutStep.Name = "__system_checkout";
|
||||
checkoutStep.Reference = new TaskStepDefinitionReference()
|
||||
{
|
||||
Id = PipelineConstants.CheckoutTask.Id,
|
||||
Name = PipelineConstants.CheckoutTask.Name,
|
||||
Version = PipelineConstants.CheckoutTask.Version,
|
||||
};
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.Repository] = "__legacy_repo_endpoint";
|
||||
|
||||
// construct self repository resource
|
||||
var defaultRepo = new RepositoryResource();
|
||||
defaultRepo.Alias = "__legacy_repo_endpoint";
|
||||
defaultRepo.Properties.Set<String>(RepositoryPropertyNames.Name, legacyRepoEndpoint.Name);
|
||||
legacyRepoEndpoint.Data.TryGetValue("repositoryId", out string repositoryId);
|
||||
if (!string.IsNullOrEmpty(repositoryId))
|
||||
{
|
||||
defaultRepo.Id = repositoryId;
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultRepo.Id = "__legacy_repo_endpoint";
|
||||
}
|
||||
|
||||
defaultRepo.Endpoint = new ServiceEndpointReference()
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Name = legacyRepoEndpoint.Name
|
||||
};
|
||||
defaultRepo.Type = ConvertLegacySourceType(legacyRepoEndpoint.Type);
|
||||
defaultRepo.Url = legacyRepoEndpoint.Url;
|
||||
if (variables.TryGetValue("build.sourceVersion", out VariableValue sourceVersion) && !string.IsNullOrEmpty(sourceVersion?.Value))
|
||||
{
|
||||
defaultRepo.Version = sourceVersion.Value;
|
||||
}
|
||||
if (variables.TryGetValue("build.sourceBranch", out VariableValue sourceBranch) && !string.IsNullOrEmpty(sourceBranch?.Value))
|
||||
{
|
||||
defaultRepo.Properties.Set<string>(RepositoryPropertyNames.Ref, sourceBranch.Value);
|
||||
}
|
||||
|
||||
VersionInfo versionInfo = null;
|
||||
if (variables.TryGetValue("build.sourceVersionAuthor", out VariableValue sourceAuthor) && !string.IsNullOrEmpty(sourceAuthor?.Value))
|
||||
{
|
||||
versionInfo = new VersionInfo();
|
||||
versionInfo.Author = sourceAuthor.Value;
|
||||
}
|
||||
if (variables.TryGetValue("build.sourceVersionMessage", out VariableValue sourceMessage) && !string.IsNullOrEmpty(sourceMessage?.Value))
|
||||
{
|
||||
if (versionInfo == null)
|
||||
{
|
||||
versionInfo = new VersionInfo();
|
||||
}
|
||||
versionInfo.Message = sourceMessage.Value;
|
||||
}
|
||||
if (versionInfo != null)
|
||||
{
|
||||
defaultRepo.Properties.Set<VersionInfo>(RepositoryPropertyNames.VersionInfo, versionInfo);
|
||||
}
|
||||
|
||||
if (defaultRepo.Type == RepositoryTypes.Tfvc)
|
||||
{
|
||||
if (variables.TryGetValue("build.sourceTfvcShelveset", out VariableValue shelveset) && !string.IsNullOrEmpty(shelveset?.Value))
|
||||
{
|
||||
defaultRepo.Properties.Set<string>(RepositoryPropertyNames.Shelveset, shelveset.Value);
|
||||
}
|
||||
|
||||
var legacyTfvcMappingJson = legacyRepoEndpoint.Data["tfvcWorkspaceMapping"];
|
||||
var legacyTfvcMapping = JsonUtility.FromString<LegacyBuildWorkspace>(legacyTfvcMappingJson);
|
||||
if (legacyTfvcMapping != null)
|
||||
{
|
||||
IList<WorkspaceMapping> tfvcMapping = new List<WorkspaceMapping>();
|
||||
foreach (var mapping in legacyTfvcMapping.Mappings)
|
||||
{
|
||||
tfvcMapping.Add(new WorkspaceMapping() { ServerPath = mapping.ServerPath, LocalPath = mapping.LocalPath, Exclude = String.Equals(mapping.MappingType, "cloak", StringComparison.OrdinalIgnoreCase) });
|
||||
}
|
||||
|
||||
defaultRepo.Properties.Set<IList<WorkspaceMapping>>(RepositoryPropertyNames.Mappings, tfvcMapping);
|
||||
}
|
||||
}
|
||||
else if (defaultRepo.Type == RepositoryTypes.Svn)
|
||||
{
|
||||
var legacySvnMappingJson = legacyRepoEndpoint.Data["svnWorkspaceMapping"];
|
||||
var legacySvnMapping = JsonUtility.FromString<LegacySvnWorkspace>(legacySvnMappingJson);
|
||||
if (legacySvnMapping != null)
|
||||
{
|
||||
IList<WorkspaceMapping> svnMapping = new List<WorkspaceMapping>();
|
||||
foreach (var mapping in legacySvnMapping.Mappings)
|
||||
{
|
||||
svnMapping.Add(new WorkspaceMapping() { ServerPath = mapping.ServerPath, LocalPath = mapping.LocalPath, Depth = mapping.Depth, IgnoreExternals = mapping.IgnoreExternals, Revision = mapping.Revision });
|
||||
}
|
||||
|
||||
defaultRepo.Properties.Set<IList<WorkspaceMapping>>(RepositoryPropertyNames.Mappings, svnMapping);
|
||||
}
|
||||
}
|
||||
|
||||
legacyRepoEndpoint.Data.TryGetValue("clean", out string cleanString);
|
||||
if (!string.IsNullOrEmpty(cleanString))
|
||||
{
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.Clean] = cleanString;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Checkout task has clean set tp false as default.
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.Clean] = Boolean.FalseString;
|
||||
}
|
||||
|
||||
if (legacyRepoEndpoint.Data.TryGetValue("checkoutSubmodules", out string checkoutSubmodulesString) &&
|
||||
Boolean.TryParse(checkoutSubmodulesString, out Boolean checkoutSubmodules) &&
|
||||
checkoutSubmodules)
|
||||
{
|
||||
if (legacyRepoEndpoint.Data.TryGetValue("checkoutNestedSubmodules", out string nestedSubmodulesString) &&
|
||||
Boolean.TryParse(nestedSubmodulesString, out Boolean nestedSubmodules) &&
|
||||
nestedSubmodules)
|
||||
{
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.Submodules] = PipelineConstants.CheckoutTaskInputs.SubmodulesOptions.Recursive;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.Submodules] = PipelineConstants.CheckoutTaskInputs.SubmodulesOptions.True;
|
||||
}
|
||||
}
|
||||
|
||||
if (legacyRepoEndpoint.Data.ContainsKey("fetchDepth"))
|
||||
{
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.FetchDepth] = legacyRepoEndpoint.Data["fetchDepth"];
|
||||
}
|
||||
|
||||
if (legacyRepoEndpoint.Data.ContainsKey("gitLfsSupport"))
|
||||
{
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.Lfs] = legacyRepoEndpoint.Data["gitLfsSupport"];
|
||||
}
|
||||
|
||||
if (VariableUtility.GetEnableAccessTokenType(variables) == EnableAccessTokenType.Variable)
|
||||
{
|
||||
checkoutStep.Inputs[PipelineConstants.CheckoutTaskInputs.PersistCredentials] = Boolean.TrueString;
|
||||
}
|
||||
|
||||
// construct worksapce option
|
||||
if (Boolean.TryParse(cleanString, out Boolean clean) && clean)
|
||||
{
|
||||
if (legacyRepoEndpoint.Data.TryGetValue("cleanOptions", out string cleanOptionsString) && !string.IsNullOrEmpty(cleanOptionsString))
|
||||
{
|
||||
if (string.Equals(cleanOptionsString, "1", StringComparison.OrdinalIgnoreCase)) //RepositoryCleanOptions.SourceAndOutputDir
|
||||
{
|
||||
workspace.Clean = PipelineConstants.WorkspaceCleanOptions.Outputs;
|
||||
}
|
||||
else if (string.Equals(cleanOptionsString, "2", StringComparison.OrdinalIgnoreCase)) //RepositoryCleanOptions.SourceDir
|
||||
{
|
||||
workspace.Clean = PipelineConstants.WorkspaceCleanOptions.Resources;
|
||||
}
|
||||
else if (string.Equals(cleanOptionsString, "3", StringComparison.OrdinalIgnoreCase)) //RepositoryCleanOptions.AllBuildDir
|
||||
{
|
||||
workspace.Clean = PipelineConstants.WorkspaceCleanOptions.All;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add checkout task when build.syncsources and skipSyncSource not set
|
||||
variables.TryGetValue("build.syncSources", out VariableValue syncSourcesVariable);
|
||||
legacyRepoEndpoint.Data.TryGetValue("skipSyncSource", out string skipSyncSource);
|
||||
if (!string.IsNullOrEmpty(syncSourcesVariable?.Value) && Boolean.TryParse(syncSourcesVariable?.Value, out bool syncSource) && !syncSource)
|
||||
{
|
||||
checkoutStep.Condition = bool.FalseString;
|
||||
}
|
||||
else if (Boolean.TryParse(skipSyncSource, out bool skipSource) && skipSource)
|
||||
{
|
||||
checkoutStep.Condition = bool.FalseString;
|
||||
}
|
||||
|
||||
jobSteps.Insert(0, checkoutStep);
|
||||
|
||||
// always add self repository to job resource
|
||||
jobResources.Repositories.Add(defaultRepo);
|
||||
}
|
||||
}
|
||||
|
||||
AgentJobRequestMessage agentRequestMessage = new AgentJobRequestMessage(message.Plan, message.Timeline, message.JobId, message.JobName, message.JobRefName, null, null, null, variables, maskHints.ToList(), jobResources, null, workspace, jobSteps, null)
|
||||
{
|
||||
RequestId = message.RequestId
|
||||
};
|
||||
|
||||
return agentRequestMessage;
|
||||
}
|
||||
|
||||
// Pipeline JobRequestMessage -> Legacy JobRequestMessage
|
||||
// Used by the server when the connected agent is old version and doesn't support new contract yet.
|
||||
public static WebApi.AgentJobRequestMessage Convert(AgentJobRequestMessage message)
|
||||
{
|
||||
// Old agent can't handle container(s)
|
||||
if (message.JobContainer != null)
|
||||
{
|
||||
throw new NotSupportedException("Job containers are not supported");
|
||||
}
|
||||
if (message.JobServiceContainers != null)
|
||||
{
|
||||
throw new NotSupportedException("Job service containers are not supported");
|
||||
}
|
||||
|
||||
// Old agent can't handle more than 1 repository
|
||||
if (message.Resources.Repositories.Count > 1)
|
||||
{
|
||||
throw new NotSupportedException(string.Join(", ", message.Resources.Repositories.Select(x => x.Alias)));
|
||||
}
|
||||
|
||||
// Old agent can't handle more than 1 checkout task
|
||||
if (message.Steps.Where(x => x.IsCheckoutTask()).Count() > 1)
|
||||
{
|
||||
throw new NotSupportedException(PipelineConstants.CheckoutTask.Id.ToString("D"));
|
||||
}
|
||||
|
||||
// construct tasks
|
||||
List<TaskInstance> tasks = new List<TaskInstance>();
|
||||
foreach (var step in message.Steps)
|
||||
{
|
||||
// Pipeline builder should add min agent demand when steps contains group
|
||||
if (step.Type != StepType.Task)
|
||||
{
|
||||
throw new NotSupportedException(step.Type.ToString());
|
||||
}
|
||||
|
||||
// don't add checkout task, we need to convert the checkout task into endpoint
|
||||
if (!step.IsCheckoutTask())
|
||||
{
|
||||
TaskInstance task = (step as TaskStep).ToLegacyTaskInstance();
|
||||
tasks.Add(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.Resources != null)
|
||||
{
|
||||
foreach (var endpoint in message.Resources.Endpoints)
|
||||
{
|
||||
// Legacy message require all endpoint's name equals to endpoint's id
|
||||
// Guid.Empty is for repository endpoints
|
||||
if (!String.Equals(endpoint.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase) &&
|
||||
endpoint.Id != Guid.Empty)
|
||||
{
|
||||
endpoint.Name = endpoint.Id.ToString("D");
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we propagate download ticket into the mask hints
|
||||
foreach (var secureFile in message.Resources.SecureFiles)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(secureFile.Ticket))
|
||||
{
|
||||
message.MaskHints.Add(new MaskHint() { Type = MaskType.Regex, Value = Regex.Escape(secureFile.Ticket) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (String.Equals(message.Plan.PlanType, "Build", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// create repository endpoint base on checkout task + repository resource + repository endpoint
|
||||
// repoResource might be null when environment verion is still on 1
|
||||
var repoResource = message.Resources?.Repositories.SingleOrDefault();
|
||||
if (repoResource != null)
|
||||
{
|
||||
var legacyRepoEndpoint = new ServiceEndpoint();
|
||||
legacyRepoEndpoint.Name = repoResource.Properties.Get<string>(RepositoryPropertyNames.Name);
|
||||
legacyRepoEndpoint.Type = ConvertToLegacySourceType(repoResource.Type);
|
||||
legacyRepoEndpoint.Url = repoResource.Url;
|
||||
if (repoResource.Endpoint != null)
|
||||
{
|
||||
var referencedEndpoint = message.Resources.Endpoints.First(x => (x.Id == repoResource.Endpoint.Id && x.Id != Guid.Empty) || (String.Equals(x.Name, repoResource.Endpoint.Name?.Literal, StringComparison.OrdinalIgnoreCase) && x.Id == Guid.Empty && repoResource.Endpoint.Id == Guid.Empty));
|
||||
var endpointAuthCopy = referencedEndpoint.Authorization?.Clone();
|
||||
if (endpointAuthCopy != null)
|
||||
{
|
||||
if (endpointAuthCopy.Scheme == EndpointAuthorizationSchemes.Token) //InstallationToken (Tabby) or ApiToken (GithubEnterprise)
|
||||
{
|
||||
if (referencedEndpoint.Authorization.Parameters.TryGetValue(EndpointAuthorizationParameters.AccessToken, out string accessToken)) //Tabby
|
||||
{
|
||||
legacyRepoEndpoint.Authorization = new EndpointAuthorization()
|
||||
{
|
||||
Scheme = EndpointAuthorizationSchemes.UsernamePassword,
|
||||
Parameters =
|
||||
{
|
||||
{ EndpointAuthorizationParameters.Username, "x-access-token" },
|
||||
{ EndpointAuthorizationParameters.Password, accessToken }
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (referencedEndpoint.Authorization.Parameters.TryGetValue(EndpointAuthorizationParameters.ApiToken, out string apiToken)) //GithubEnterprise
|
||||
{
|
||||
legacyRepoEndpoint.Authorization = new EndpointAuthorization()
|
||||
{
|
||||
Scheme = EndpointAuthorizationSchemes.UsernamePassword,
|
||||
Parameters =
|
||||
{
|
||||
{ EndpointAuthorizationParameters.Username, apiToken },
|
||||
{ EndpointAuthorizationParameters.Password, "x-oauth-basic" }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (endpointAuthCopy.Scheme == EndpointAuthorizationSchemes.PersonalAccessToken) // Github
|
||||
{
|
||||
if (referencedEndpoint.Authorization.Parameters.TryGetValue(EndpointAuthorizationParameters.AccessToken, out string accessToken)) //Tabby
|
||||
{
|
||||
legacyRepoEndpoint.Authorization = new EndpointAuthorization()
|
||||
{
|
||||
Scheme = EndpointAuthorizationSchemes.UsernamePassword,
|
||||
Parameters =
|
||||
{
|
||||
{ EndpointAuthorizationParameters.Username, "pat" },
|
||||
{ EndpointAuthorizationParameters.Password, accessToken }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
legacyRepoEndpoint.Authorization = endpointAuthCopy;
|
||||
}
|
||||
}
|
||||
|
||||
// there are 2 properties we put into the legacy repo endpoint directly from connect endpoint
|
||||
if (referencedEndpoint.Data.TryGetValue("acceptUntrustedCerts", out String acceptUntrustedCerts))
|
||||
{
|
||||
legacyRepoEndpoint.Data["acceptUntrustedCerts"] = acceptUntrustedCerts;
|
||||
}
|
||||
if (referencedEndpoint.Data.TryGetValue("realmName", out String realmName))
|
||||
{
|
||||
legacyRepoEndpoint.Data["realmName"] = realmName;
|
||||
}
|
||||
}
|
||||
legacyRepoEndpoint.Data["repositoryId"] = repoResource.Id;
|
||||
|
||||
// default values in the old message format
|
||||
legacyRepoEndpoint.Data["clean"] = Boolean.FalseString;
|
||||
legacyRepoEndpoint.Data["checkoutSubmodules"] = Boolean.FalseString;
|
||||
legacyRepoEndpoint.Data["checkoutNestedSubmodules"] = Boolean.FalseString;
|
||||
legacyRepoEndpoint.Data["fetchDepth"] = "0";
|
||||
legacyRepoEndpoint.Data["gitLfsSupport"] = Boolean.FalseString;
|
||||
legacyRepoEndpoint.Data["skipSyncSource"] = Boolean.FalseString;
|
||||
legacyRepoEndpoint.Data["cleanOptions"] = "0";
|
||||
legacyRepoEndpoint.Data["rootFolder"] = null; // old tfvc repo endpoint has this set to $/foo, but it doesn't seems to be used at all.
|
||||
|
||||
if (repoResource.Type == RepositoryTypes.Tfvc)
|
||||
{
|
||||
var tfvcMapping = repoResource.Properties.Get<IList<WorkspaceMapping>>(RepositoryPropertyNames.Mappings);
|
||||
if (tfvcMapping != null)
|
||||
{
|
||||
LegacyBuildWorkspace legacyMapping = new LegacyBuildWorkspace();
|
||||
foreach (var mapping in tfvcMapping)
|
||||
{
|
||||
legacyMapping.Mappings.Add(new LegacyMappingDetails() { ServerPath = mapping.ServerPath, LocalPath = mapping.LocalPath, MappingType = mapping.Exclude ? "cloak" : "map" });
|
||||
}
|
||||
|
||||
legacyRepoEndpoint.Data["tfvcWorkspaceMapping"] = JsonUtility.ToString(legacyMapping);
|
||||
}
|
||||
}
|
||||
else if (repoResource.Type == RepositoryTypes.Svn)
|
||||
{
|
||||
var svnMapping = repoResource.Properties.Get<IList<WorkspaceMapping>>(RepositoryPropertyNames.Mappings);
|
||||
if (svnMapping != null)
|
||||
{
|
||||
LegacySvnWorkspace legacyMapping = new LegacySvnWorkspace();
|
||||
foreach (var mapping in svnMapping)
|
||||
{
|
||||
legacyMapping.Mappings.Add(new LegacySvnMappingDetails() { ServerPath = mapping.ServerPath, LocalPath = mapping.LocalPath, Depth = mapping.Depth, IgnoreExternals = mapping.IgnoreExternals, Revision = mapping.Revision });
|
||||
}
|
||||
|
||||
legacyRepoEndpoint.Data["svnWorkspaceMapping"] = JsonUtility.ToString(legacyMapping);
|
||||
}
|
||||
}
|
||||
else if (repoResource.Type == RepositoryTypes.Git)
|
||||
{
|
||||
if (message.Variables.TryGetValue(WellKnownDistributedTaskVariables.ServerType, out VariableValue serverType) && String.Equals(serverType?.Value, "Hosted", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
legacyRepoEndpoint.Data["onpremtfsgit"] = Boolean.FalseString;
|
||||
}
|
||||
else
|
||||
{
|
||||
legacyRepoEndpoint.Data["onpremtfsgit"] = Boolean.TrueString;
|
||||
}
|
||||
}
|
||||
|
||||
if (!message.Variables.ContainsKey("build.repository.id") || String.IsNullOrEmpty(message.Variables["build.repository.id"]?.Value))
|
||||
{
|
||||
message.Variables["build.repository.id"] = repoResource.Id;
|
||||
}
|
||||
if (!message.Variables.ContainsKey("build.repository.name") || String.IsNullOrEmpty(message.Variables["build.repository.name"]?.Value))
|
||||
{
|
||||
message.Variables["build.repository.name"] = repoResource.Properties.Get<String>(RepositoryPropertyNames.Name);
|
||||
}
|
||||
if (!message.Variables.ContainsKey("build.repository.uri") || String.IsNullOrEmpty(message.Variables["build.repository.uri"]?.Value))
|
||||
{
|
||||
message.Variables["build.repository.uri"] = repoResource.Url.AbsoluteUri;
|
||||
}
|
||||
|
||||
var versionInfo = repoResource.Properties.Get<VersionInfo>(RepositoryPropertyNames.VersionInfo);
|
||||
if (!message.Variables.ContainsKey("build.sourceVersionAuthor") || String.IsNullOrEmpty(message.Variables["build.sourceVersionAuthor"]?.Value))
|
||||
{
|
||||
message.Variables["build.sourceVersionAuthor"] = versionInfo?.Author;
|
||||
}
|
||||
if (!message.Variables.ContainsKey("build.sourceVersionMessage") || String.IsNullOrEmpty(message.Variables["build.sourceVersionMessage"]?.Value))
|
||||
{
|
||||
message.Variables["build.sourceVersionMessage"] = versionInfo?.Message;
|
||||
}
|
||||
if (!message.Variables.ContainsKey("build.sourceVersion") || String.IsNullOrEmpty(message.Variables["build.sourceVersion"]?.Value))
|
||||
{
|
||||
message.Variables["build.sourceVersion"] = repoResource.Version;
|
||||
}
|
||||
if (!message.Variables.ContainsKey("build.sourceBranch") || String.IsNullOrEmpty(message.Variables["build.sourceBranch"]?.Value))
|
||||
{
|
||||
message.Variables["build.sourceBranch"] = repoResource.Properties.Get<String>(RepositoryPropertyNames.Ref);
|
||||
}
|
||||
if (repoResource.Type == RepositoryTypes.Tfvc)
|
||||
{
|
||||
var shelveset = repoResource.Properties.Get<String>(RepositoryPropertyNames.Shelveset);
|
||||
if (!String.IsNullOrEmpty(shelveset) && (!message.Variables.ContainsKey("build.sourceTfvcShelveset") || String.IsNullOrEmpty(message.Variables["build.sourceTfvcShelveset"]?.Value)))
|
||||
{
|
||||
message.Variables["build.sourceTfvcShelveset"] = shelveset;
|
||||
}
|
||||
}
|
||||
|
||||
TaskStep checkoutTask = message.Steps.FirstOrDefault(x => x.IsCheckoutTask()) as TaskStep;
|
||||
if (checkoutTask != null)
|
||||
{
|
||||
if (checkoutTask.Inputs.TryGetValue(PipelineConstants.CheckoutTaskInputs.Clean, out string taskInputClean) && !string.IsNullOrEmpty(taskInputClean))
|
||||
{
|
||||
legacyRepoEndpoint.Data["clean"] = taskInputClean;
|
||||
}
|
||||
else
|
||||
{
|
||||
legacyRepoEndpoint.Data["clean"] = Boolean.FalseString;
|
||||
}
|
||||
|
||||
if (checkoutTask.Inputs.TryGetValue(PipelineConstants.CheckoutTaskInputs.Submodules, out string taskInputSubmodules) && !string.IsNullOrEmpty(taskInputSubmodules))
|
||||
{
|
||||
legacyRepoEndpoint.Data["checkoutSubmodules"] = Boolean.TrueString;
|
||||
if (String.Equals(taskInputSubmodules, PipelineConstants.CheckoutTaskInputs.SubmodulesOptions.Recursive, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
legacyRepoEndpoint.Data["checkoutNestedSubmodules"] = Boolean.TrueString;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkoutTask.Inputs.TryGetValue(PipelineConstants.CheckoutTaskInputs.FetchDepth, out string taskInputFetchDepth) && !string.IsNullOrEmpty(taskInputFetchDepth))
|
||||
{
|
||||
legacyRepoEndpoint.Data["fetchDepth"] = taskInputFetchDepth;
|
||||
}
|
||||
|
||||
if (checkoutTask.Inputs.TryGetValue(PipelineConstants.CheckoutTaskInputs.Lfs, out string taskInputfs) && !string.IsNullOrEmpty(taskInputfs))
|
||||
{
|
||||
legacyRepoEndpoint.Data["gitLfsSupport"] = taskInputfs;
|
||||
}
|
||||
|
||||
// Skip sync sources
|
||||
if (String.Equals(checkoutTask.Inputs[PipelineConstants.CheckoutTaskInputs.Repository], PipelineConstants.NoneAlias, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
legacyRepoEndpoint.Data["skipSyncSource"] = Boolean.TrueString;
|
||||
}
|
||||
else if (String.Equals(checkoutTask.Inputs[PipelineConstants.CheckoutTaskInputs.Repository], PipelineConstants.DesignerRepo, StringComparison.OrdinalIgnoreCase) && checkoutTask.Condition == Boolean.FalseString)
|
||||
{
|
||||
legacyRepoEndpoint.Data["skipSyncSource"] = Boolean.TrueString;
|
||||
}
|
||||
}
|
||||
|
||||
// workspace clean options
|
||||
legacyRepoEndpoint.Data["cleanOptions"] = "0"; // RepositoryCleanOptions.Source;
|
||||
if (message.Workspace != null)
|
||||
{
|
||||
if (String.Equals(message.Workspace.Clean, PipelineConstants.WorkspaceCleanOptions.Outputs, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
legacyRepoEndpoint.Data["cleanOptions"] = "1"; // RepositoryCleanOptions.SourceAndOutputDir;
|
||||
}
|
||||
else if (String.Equals(message.Workspace.Clean, PipelineConstants.WorkspaceCleanOptions.Resources, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
legacyRepoEndpoint.Data["cleanOptions"] = "2"; //RepositoryCleanOptions.SourceDir;
|
||||
}
|
||||
else if (String.Equals(message.Workspace.Clean, PipelineConstants.WorkspaceCleanOptions.All, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
legacyRepoEndpoint.Data["cleanOptions"] = "3"; // RepositoryCleanOptions.AllBuildDir;
|
||||
}
|
||||
}
|
||||
|
||||
// add reposiotry endpoint to environment
|
||||
message.Resources.Endpoints.Add(legacyRepoEndpoint);
|
||||
}
|
||||
}
|
||||
|
||||
JobEnvironment environment = new JobEnvironment(message.Variables, message.MaskHints, message.Resources);
|
||||
|
||||
WebApi.AgentJobRequestMessage legacyAgentRequestMessage = new WebApi.AgentJobRequestMessage(message.Plan, message.Timeline, message.JobId, message.JobDisplayName, message.JobName, environment, tasks)
|
||||
{
|
||||
RequestId = message.RequestId
|
||||
};
|
||||
|
||||
return legacyAgentRequestMessage;
|
||||
}
|
||||
|
||||
private static string ConvertLegacySourceType(string legacySourceType)
|
||||
{
|
||||
if (String.Equals(legacySourceType, LegacyRepositoryTypes.Bitbucket, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RepositoryTypes.Bitbucket;
|
||||
}
|
||||
else if (String.Equals(legacySourceType, LegacyRepositoryTypes.Git, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RepositoryTypes.ExternalGit;
|
||||
}
|
||||
else if (String.Equals(legacySourceType, LegacyRepositoryTypes.TfsGit, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RepositoryTypes.Git;
|
||||
}
|
||||
else if (String.Equals(legacySourceType, LegacyRepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RepositoryTypes.GitHub;
|
||||
}
|
||||
else if (String.Equals(legacySourceType, LegacyRepositoryTypes.GitHubEnterprise, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RepositoryTypes.GitHubEnterprise;
|
||||
}
|
||||
else if (String.Equals(legacySourceType, LegacyRepositoryTypes.Svn, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RepositoryTypes.Svn;
|
||||
}
|
||||
else if (String.Equals(legacySourceType, LegacyRepositoryTypes.TfsVersionControl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return RepositoryTypes.Tfvc;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException(legacySourceType);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertToLegacySourceType(string pipelineSourceType)
|
||||
{
|
||||
if (String.Equals(pipelineSourceType, RepositoryTypes.Bitbucket, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LegacyRepositoryTypes.Bitbucket;
|
||||
}
|
||||
else if (String.Equals(pipelineSourceType, RepositoryTypes.ExternalGit, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LegacyRepositoryTypes.Git;
|
||||
}
|
||||
else if (String.Equals(pipelineSourceType, RepositoryTypes.Git, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LegacyRepositoryTypes.TfsGit;
|
||||
}
|
||||
else if (String.Equals(pipelineSourceType, RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LegacyRepositoryTypes.GitHub;
|
||||
}
|
||||
else if (String.Equals(pipelineSourceType, RepositoryTypes.GitHubEnterprise, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LegacyRepositoryTypes.GitHubEnterprise;
|
||||
}
|
||||
else if (String.Equals(pipelineSourceType, RepositoryTypes.Svn, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LegacyRepositoryTypes.Svn;
|
||||
}
|
||||
else if (String.Equals(pipelineSourceType, RepositoryTypes.Tfvc, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LegacyRepositoryTypes.TfsVersionControl;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException(pipelineSourceType);
|
||||
}
|
||||
}
|
||||
|
||||
private static class LegacyRepositoryTypes // Copy from Build.Webapi
|
||||
{
|
||||
public const String TfsVersionControl = "TfsVersionControl";
|
||||
public const String TfsGit = "TfsGit";
|
||||
public const String Git = "Git";
|
||||
public const String GitHub = "GitHub";
|
||||
public const String GitHubEnterprise = "GitHubEnterprise";
|
||||
public const String Bitbucket = "Bitbucket";
|
||||
public const String Svn = "Svn";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entry in a workspace mapping.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
private class LegacyMappingDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// The server path.
|
||||
/// </summary>
|
||||
[DataMember(Name = "serverPath")]
|
||||
public String ServerPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The mapping type.
|
||||
/// </summary>
|
||||
[DataMember(Name = "mappingType")]
|
||||
public String MappingType
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The local path.
|
||||
/// </summary>
|
||||
[DataMember(Name = "localPath")]
|
||||
public String LocalPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a workspace mapping.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
private class LegacyBuildWorkspace
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of workspace mapping entries.
|
||||
/// </summary>
|
||||
public List<LegacyMappingDetails> Mappings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_mappings == null)
|
||||
{
|
||||
m_mappings = new List<LegacyMappingDetails>();
|
||||
}
|
||||
return m_mappings;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "mappings")]
|
||||
private List<LegacyMappingDetails> m_mappings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Subversion mapping entry.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
private class LegacySvnMappingDetails
|
||||
{
|
||||
/// <summary>
|
||||
/// The server path.
|
||||
/// </summary>
|
||||
[DataMember(Name = "serverPath")]
|
||||
public String ServerPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The local path.
|
||||
/// </summary>
|
||||
[DataMember(Name = "localPath")]
|
||||
public String LocalPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The revision.
|
||||
/// </summary>
|
||||
[DataMember(Name = "revision")]
|
||||
public String Revision
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The depth.
|
||||
/// </summary>
|
||||
[DataMember(Name = "depth")]
|
||||
public Int32 Depth
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether to ignore externals.
|
||||
/// </summary>
|
||||
[DataMember(Name = "ignoreExternals")]
|
||||
public bool IgnoreExternals
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a subversion workspace.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
private class LegacySvnWorkspace
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of mappings.
|
||||
/// </summary>
|
||||
public List<LegacySvnMappingDetails> Mappings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_Mappings == null)
|
||||
{
|
||||
m_Mappings = new List<LegacySvnMappingDetails>();
|
||||
}
|
||||
return m_Mappings;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "mappings")]
|
||||
private List<LegacySvnMappingDetails> m_Mappings;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/Sdk/DTPipelines/Pipelines/AgentPoolReference.cs
Normal file
38
src/Sdk/DTPipelines/Pipelines/AgentPoolReference.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class AgentPoolReference : ResourceReference
|
||||
{
|
||||
public AgentPoolReference()
|
||||
{
|
||||
}
|
||||
|
||||
private AgentPoolReference(AgentPoolReference referenceToCopy)
|
||||
: base(referenceToCopy)
|
||||
{
|
||||
this.Id = referenceToCopy.Id;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Int32 Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public AgentPoolReference Clone()
|
||||
{
|
||||
return new AgentPoolReference(this);
|
||||
}
|
||||
|
||||
public override String ToString()
|
||||
{
|
||||
return base.ToString() ?? this.Id.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
116
src/Sdk/DTPipelines/Pipelines/AgentPoolStore.cs
Normal file
116
src/Sdk/DTPipelines/Pipelines/AgentPoolStore.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class AgentPoolStore : IAgentPoolStore
|
||||
{
|
||||
public AgentPoolStore(
|
||||
IList<TaskAgentPool> pools,
|
||||
IAgentPoolResolver resolver = null)
|
||||
{
|
||||
this.Resolver = resolver;
|
||||
Add(pools?.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the queue resolver configured for this store.
|
||||
/// </summary>
|
||||
public IAgentPoolResolver Resolver
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public void Authorize(IList<AgentPoolReference> pools)
|
||||
{
|
||||
if (pools?.Count > 0)
|
||||
{
|
||||
foreach (var pool in pools)
|
||||
{
|
||||
var authorizedResource = this.Resolver?.Resolve(pool);
|
||||
if (authorizedResource != null)
|
||||
{
|
||||
Add(authorizedResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IList<AgentPoolReference> GetAuthorizedReferences()
|
||||
{
|
||||
return m_resourcesById.Values.Select(x => new AgentPoolReference { Id = x.Id }).ToList();
|
||||
}
|
||||
|
||||
public TaskAgentPool Get(AgentPoolReference reference)
|
||||
{
|
||||
if (reference == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var referenceId = reference.Id;
|
||||
var referenceName = reference.Name?.Literal;
|
||||
if (reference.Id == 0 && String.IsNullOrEmpty(referenceName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
TaskAgentPool authorizedResource = null;
|
||||
if (referenceId != 0)
|
||||
{
|
||||
if (m_resourcesById.TryGetValue(referenceId, out authorizedResource))
|
||||
{
|
||||
return authorizedResource;
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrEmpty(referenceName))
|
||||
{
|
||||
if (m_resourcesByName.TryGetValue(referenceName, out authorizedResource))
|
||||
{
|
||||
return authorizedResource;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an authorizer then attempt to authorize the reference for use
|
||||
authorizedResource = this.Resolver?.Resolve(reference);
|
||||
if (authorizedResource != null)
|
||||
{
|
||||
Add(authorizedResource);
|
||||
}
|
||||
|
||||
return authorizedResource;
|
||||
}
|
||||
|
||||
private void Add(params TaskAgentPool[] resources)
|
||||
{
|
||||
if (resources?.Length > 0)
|
||||
{
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
// Track by ID
|
||||
if (m_resourcesById.TryGetValue(resource.Id, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
m_resourcesById.Add(resource.Id, resource);
|
||||
|
||||
// Track by name
|
||||
if (m_resourcesByName.TryGetValue(resource.Name, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
m_resourcesByName.Add(resource.Name, resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<Int32, TaskAgentPool> m_resourcesById = new Dictionary<Int32, TaskAgentPool>();
|
||||
private readonly Dictionary<String, TaskAgentPool> m_resourcesByName = new Dictionary<String, TaskAgentPool>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
169
src/Sdk/DTPipelines/Pipelines/AgentPoolTarget.cs
Normal file
169
src/Sdk/DTPipelines/Pipelines/AgentPoolTarget.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
using GitHub.DistributedTask.Pipelines.Validation;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.Common;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class AgentPoolTarget : PhaseTarget
|
||||
{
|
||||
public AgentPoolTarget()
|
||||
: base(PhaseTargetType.Pool)
|
||||
{
|
||||
}
|
||||
|
||||
private AgentPoolTarget(AgentPoolTarget targetToClone)
|
||||
: base(targetToClone)
|
||||
{
|
||||
this.Pool = targetToClone.Pool?.Clone();
|
||||
|
||||
|
||||
if (targetToClone.AgentSpecification != null)
|
||||
{
|
||||
this.AgentSpecification = new JObject(targetToClone.AgentSpecification);
|
||||
}
|
||||
|
||||
if (targetToClone.m_agentIds?.Count > 0)
|
||||
{
|
||||
this.m_agentIds = targetToClone.m_agentIds;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target pool from which agents will be selected.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public AgentPoolReference Pool
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public JObject AgentSpecification
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets agent Ids filter on which deployment should be done.
|
||||
/// </summary>
|
||||
public List<Int32> AgentIds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_agentIds == null)
|
||||
{
|
||||
m_agentIds = new List<Int32>();
|
||||
}
|
||||
return m_agentIds;
|
||||
}
|
||||
}
|
||||
|
||||
public override PhaseTarget Clone()
|
||||
{
|
||||
return new AgentPoolTarget(this);
|
||||
}
|
||||
|
||||
public override Boolean IsValid(TaskDefinition task)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(task, nameof(task));
|
||||
return task.RunsOn.Contains(TaskRunsOnConstants.RunsOnAgent, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal override void Validate(
|
||||
IPipelineContext context,
|
||||
BuildOptions buildOptions,
|
||||
ValidationResult result,
|
||||
IList<Step> steps,
|
||||
ISet<Demand> taskDemands)
|
||||
{
|
||||
// validate pool
|
||||
Int32 poolId = 0;
|
||||
String poolName = null;
|
||||
var pool = this.Pool;
|
||||
if (pool != null)
|
||||
{
|
||||
poolId = pool.Id;
|
||||
poolName = pool.Name?.GetValue(context)?.Value;
|
||||
}
|
||||
|
||||
if (poolId == 0 && String.IsNullOrEmpty(poolName) && buildOptions.ValidateResources)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotDefined()));
|
||||
}
|
||||
else
|
||||
{
|
||||
// we have a valid queue. record the reference
|
||||
result.AddPoolReference(poolId, poolName);
|
||||
|
||||
// Attempt to resolve the queue using any identifier specified. We will look up by either ID
|
||||
// or name and the ID is preferred since it is immutable and more specific.
|
||||
if (buildOptions.ValidateResources)
|
||||
{
|
||||
TaskAgentPool taskAgentPool = null;
|
||||
var resourceStore = context.ResourceStore;
|
||||
if (resourceStore != null)
|
||||
{
|
||||
if (poolId != 0)
|
||||
{
|
||||
taskAgentPool = resourceStore.GetPool(poolId);
|
||||
if (taskAgentPool == null)
|
||||
{
|
||||
result.UnauthorizedResources.Pools.Add(new AgentPoolReference { Id = poolId });
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotFound(poolId)));
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrEmpty(poolName))
|
||||
{
|
||||
taskAgentPool = resourceStore.GetPool(poolName);
|
||||
if (taskAgentPool == null)
|
||||
{
|
||||
result.UnauthorizedResources.Pools.Add(new AgentPoolReference { Name = poolName });
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotFound(poolName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the resolved values inline to the resolved resource for this validation run
|
||||
if (taskAgentPool != null)
|
||||
{
|
||||
this.Pool.Id = taskAgentPool.Id;
|
||||
this.Pool.Name = taskAgentPool.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal override JobExecutionContext CreateJobContext(PhaseExecutionContext context, string jobName, int attempt, bool continueOnError, int timeoutInMinutes, int cancelTimeoutInMinutes, IJobFactory jobFactory)
|
||||
{
|
||||
throw new NotSupportedException(nameof(AgentPoolTarget));
|
||||
}
|
||||
|
||||
internal override ExpandPhaseResult Expand(PhaseExecutionContext context, bool continueOnError, int timeoutInMinutes, int cancelTimeoutInMinutes, IJobFactory jobFactory, JobExpansionOptions options)
|
||||
{
|
||||
throw new NotSupportedException(nameof(AgentPoolTarget));
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_agentIds?.Count == 0)
|
||||
{
|
||||
m_agentIds = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "AgentIds", EmitDefaultValue = false)]
|
||||
private List<Int32> m_agentIds;
|
||||
}
|
||||
}
|
||||
38
src/Sdk/DTPipelines/Pipelines/AgentQueueReference.cs
Normal file
38
src/Sdk/DTPipelines/Pipelines/AgentQueueReference.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class AgentQueueReference : ResourceReference
|
||||
{
|
||||
public AgentQueueReference()
|
||||
{
|
||||
}
|
||||
|
||||
private AgentQueueReference(AgentQueueReference referenceToCopy)
|
||||
: base(referenceToCopy)
|
||||
{
|
||||
this.Id = referenceToCopy.Id;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Int32 Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public AgentQueueReference Clone()
|
||||
{
|
||||
return new AgentQueueReference(this);
|
||||
}
|
||||
|
||||
public override String ToString()
|
||||
{
|
||||
return base.ToString() ?? this.Id.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/Sdk/DTPipelines/Pipelines/AgentQueueStore.cs
Normal file
161
src/Sdk/DTPipelines/Pipelines/AgentQueueStore.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class AgentQueueStore : IAgentQueueStore
|
||||
{
|
||||
public AgentQueueStore(
|
||||
IList<TaskAgentQueue> queues,
|
||||
IAgentQueueResolver resolver = null)
|
||||
{
|
||||
this.Resolver = resolver;
|
||||
Add(queues?.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the queue resolver configured for this store.
|
||||
/// </summary>
|
||||
public IAgentQueueResolver Resolver
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public void Authorize(IList<TaskAgentQueue> queues)
|
||||
{
|
||||
if (queues?.Count > 0)
|
||||
{
|
||||
foreach (var queue in queues)
|
||||
{
|
||||
Add(queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IList<AgentQueueReference> GetAuthorizedReferences()
|
||||
{
|
||||
return m_resourcesById.Values.Select(x => new AgentQueueReference { Id = x.Id }).ToList();
|
||||
}
|
||||
|
||||
public TaskAgentQueue Get(AgentQueueReference reference)
|
||||
{
|
||||
if (reference == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var referenceId = reference.Id;
|
||||
var referenceName = reference.Name?.Literal;
|
||||
if (reference.Id == 0 && String.IsNullOrEmpty(referenceName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
TaskAgentQueue authorizedResource = null;
|
||||
if (referenceId != 0)
|
||||
{
|
||||
if (m_resourcesById.TryGetValue(referenceId, out authorizedResource))
|
||||
{
|
||||
return authorizedResource;
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrEmpty(referenceName))
|
||||
{
|
||||
if (m_resourcesByName.TryGetValue(referenceName, out List<TaskAgentQueue> matchingResources))
|
||||
{
|
||||
if (matchingResources.Count > 1)
|
||||
{
|
||||
throw new AmbiguousResourceSpecificationException(PipelineStrings.AmbiguousServiceEndpointSpecification(referenceId));
|
||||
}
|
||||
|
||||
return matchingResources[0];
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an authorizer then attempt to authorize the reference for use
|
||||
authorizedResource = this.Resolver?.Resolve(reference);
|
||||
if (authorizedResource != null)
|
||||
{
|
||||
Add(authorizedResource);
|
||||
}
|
||||
|
||||
return authorizedResource;
|
||||
}
|
||||
|
||||
private void Add(params TaskAgentQueue[] resources)
|
||||
{
|
||||
if (resources?.Length > 0)
|
||||
{
|
||||
foreach (var resource in resources)
|
||||
{
|
||||
// Track by ID
|
||||
if (m_resourcesById.TryGetValue(resource.Id, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
m_resourcesById.Add(resource.Id, resource);
|
||||
|
||||
// not all references have names
|
||||
var name = resource.Name;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track by name
|
||||
if (!m_resourcesByName.TryGetValue(name, out var list))
|
||||
{
|
||||
list = new List<TaskAgentQueue>();
|
||||
m_resourcesByName.Add(name, list);
|
||||
}
|
||||
|
||||
// Clobber previously added alternate name, with the real hosted queue.
|
||||
// For example, during the "Hosted macOS High Sierra" transition, until the real queue
|
||||
// existed, it was treated as an alternate name for the "Hosted macOS" queue. After the
|
||||
// real "Hosted macOS High Sierra" queue was created, it took priority.
|
||||
if (list.Count > 0 && list[0].Pool?.IsHosted == true && resource.Pool?.IsHosted == true)
|
||||
{
|
||||
list[0] = resource;
|
||||
}
|
||||
// Otherwise add the queue
|
||||
else
|
||||
{
|
||||
list.Add(resource);
|
||||
}
|
||||
|
||||
// Track by alternate name for specific hosted pools.
|
||||
// For example, "Hosted macOS Preview" and "Hosted macOS" are equivalent.
|
||||
if (resource.Pool?.IsHosted == true && s_alternateNames.TryGetValue(name, out var alternateNames))
|
||||
{
|
||||
foreach (var alternateName in alternateNames)
|
||||
{
|
||||
if (!m_resourcesByName.TryGetValue(alternateName, out list))
|
||||
{
|
||||
list = new List<TaskAgentQueue>();
|
||||
m_resourcesByName.Add(alternateName, list);
|
||||
}
|
||||
|
||||
if (list.Count == 0 || list[0].Pool?.IsHosted != true)
|
||||
{
|
||||
list.Add(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<String, String[]> s_alternateNames = new Dictionary<String, String[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "Hosted macOS", new[] { "Hosted macOS Preview" } },
|
||||
{ "Hosted macOS Preview", new[] { "Hosted macOS" } },
|
||||
};
|
||||
private readonly Dictionary<Int32, TaskAgentQueue> m_resourcesById = new Dictionary<Int32, TaskAgentQueue>();
|
||||
private readonly Dictionary<String, List<TaskAgentQueue>> m_resourcesByName = new Dictionary<String, List<TaskAgentQueue>>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
647
src/Sdk/DTPipelines/Pipelines/AgentQueueTarget.cs
Normal file
647
src/Sdk/DTPipelines/Pipelines/AgentQueueTarget.cs
Normal file
@@ -0,0 +1,647 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
using GitHub.DistributedTask.Pipelines.Validation;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides options for phase execution on an agent within a queue.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class AgentQueueTarget : PhaseTarget
|
||||
{
|
||||
public AgentQueueTarget()
|
||||
: base(PhaseTargetType.Queue)
|
||||
{
|
||||
}
|
||||
|
||||
private AgentQueueTarget(AgentQueueTarget targetToClone)
|
||||
: base(targetToClone)
|
||||
{
|
||||
this.Queue = targetToClone.Queue?.Clone();
|
||||
this.Execution = targetToClone.Execution?.Clone();
|
||||
|
||||
if (targetToClone.AgentSpecification != null)
|
||||
{
|
||||
this.AgentSpecification = new JObject(targetToClone.AgentSpecification);
|
||||
}
|
||||
|
||||
if (targetToClone.SidecarContainers?.Count > 0)
|
||||
{
|
||||
m_sidecarContainers = new Dictionary<String, ExpressionValue<String>>(targetToClone.SidecarContainers, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target queue from which agents will be selected.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
[JsonConverter(typeof(QueueJsonConverter))]
|
||||
public AgentQueueReference Queue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public JObject AgentSpecification
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets parallel execution options which control expansion and execution of the phase.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public ParallelExecutionOptions Execution
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets workspace options which control how agent manage the workspace of the phase.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public WorkspaceOptions Workspace
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the container the phase will be run in.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
[JsonConverter(typeof(ExpressionValueJsonConverter<String>))]
|
||||
public ExpressionValue<String> Container
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sidecar containers that will run alongside the phase.
|
||||
/// </summary>
|
||||
public IDictionary<String, ExpressionValue<String>> SidecarContainers
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_sidecarContainers == null)
|
||||
{
|
||||
m_sidecarContainers = new Dictionary<String, ExpressionValue<String>>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
return m_sidecarContainers;
|
||||
}
|
||||
}
|
||||
|
||||
public override PhaseTarget Clone()
|
||||
{
|
||||
return new AgentQueueTarget(this);
|
||||
}
|
||||
|
||||
public override Boolean IsValid(TaskDefinition task)
|
||||
{
|
||||
ArgumentUtility.CheckForNull(task, nameof(task));
|
||||
return task.RunsOn.Contains(TaskRunsOnConstants.RunsOnAgent, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a clone of this and attempts to resolve all expressions and macros.
|
||||
/// </summary>
|
||||
internal AgentQueueTarget Evaluate(
|
||||
IPipelineContext context,
|
||||
ValidationResult result)
|
||||
{
|
||||
var qname = String.Empty;
|
||||
try
|
||||
{
|
||||
qname = context.ExpandVariables(this.Queue?.Name?.GetValue(context).Value);
|
||||
}
|
||||
catch (DistributedTask.Expressions.ExpressionException ee)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(ee.Message));
|
||||
return null;
|
||||
}
|
||||
|
||||
var literalTarget = this.Clone() as AgentQueueTarget;
|
||||
|
||||
var spec = this.AgentSpecification;
|
||||
if (spec != null)
|
||||
{
|
||||
spec = context.Evaluate(this.AgentSpecification).Value;
|
||||
literalTarget.AgentSpecification = spec;
|
||||
}
|
||||
|
||||
// Note! The "vmImage" token of the agent spec is currently treated specially.
|
||||
// This is a temporary relationship that allows vmImage agent specs to specify
|
||||
// the hosted pool to use.
|
||||
// It would be better to factor out this work into a separate, plug-in validator.
|
||||
if (String.IsNullOrEmpty(qname) && spec != null)
|
||||
{
|
||||
const string VMImage = "vmImage"; // should be: YamlConstants.VMImage, which is inaccessible :(
|
||||
spec.TryGetValue(VMImage, out var token);
|
||||
if (token != null && token.Type == JTokenType.String)
|
||||
{
|
||||
var rawTokenValue = token.Value<String>();
|
||||
var resolvedPoolName = PoolNameForVMImage(rawTokenValue);
|
||||
if (resolvedPoolName == null)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError($"Unexpected vmImage '{rawTokenValue}'"));
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
spec.Remove(VMImage);
|
||||
literalTarget.Queue = new AgentQueueReference
|
||||
{
|
||||
Name = resolvedPoolName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
literalTarget.Queue.Name = qname;
|
||||
}
|
||||
|
||||
return literalTarget;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns true for strings structured like expressions or macros.
|
||||
/// they could techincally be literals though.
|
||||
/// </summary>
|
||||
internal static Boolean IsProbablyExpressionOrMacro(String s)
|
||||
{
|
||||
return ExpressionValue.IsExpression(s) || VariableUtility.IsVariable(s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns true if this model is composed only of literal values (no expressions)
|
||||
/// </summary>
|
||||
internal Boolean IsLiteral()
|
||||
{
|
||||
var queue = this.Queue;
|
||||
if (queue != null)
|
||||
{
|
||||
var queueName = queue.Name;
|
||||
if (queueName != null)
|
||||
{
|
||||
if (!queueName.IsLiteral || VariableUtility.IsVariable(queueName.Literal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var spec = this.AgentSpecification;
|
||||
if (spec != null)
|
||||
{
|
||||
bool IsLiteral(JObject o)
|
||||
{
|
||||
foreach (var pair in o)
|
||||
{
|
||||
switch (pair.Value.Type)
|
||||
{
|
||||
case JTokenType.String:
|
||||
if (IsProbablyExpressionOrMacro(pair.Value.Value<String>()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case JTokenType.Object:
|
||||
if (!IsLiteral(pair.Value.Value<JObject>()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsLiteral(spec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporary code to translate vmImage. Pool providers work will move this to a different layer
|
||||
/// </summary>
|
||||
/// <param name="vmImageValue"></param>
|
||||
/// <returns>Hosted pool name</returns>
|
||||
internal static String PoolNameForVMImage(String vmImageValue)
|
||||
{
|
||||
switch ((vmImageValue ?? String.Empty).ToUpperInvariant())
|
||||
{
|
||||
case "UBUNTU 16.04":
|
||||
case "UBUNTU-16.04":
|
||||
case "UBUNTU LATEST":
|
||||
case "UBUNTU-LATEST":
|
||||
return "Hosted Ubuntu 1604";
|
||||
case "UBUNTU 18.04":
|
||||
case "UBUNTU-18.04":
|
||||
return "Hosted Ubuntu 1804";
|
||||
case "VISUAL STUDIO 2015 ON WINDOWS SERVER 2012R2":
|
||||
case "VS2015-WIN2012R2":
|
||||
return "Hosted";
|
||||
case "VISUAL STUDIO 2017 ON WINDOWS SERVER 2016":
|
||||
case "VS2017-WIN2016":
|
||||
return "Hosted VS2017";
|
||||
case "WINDOWS-2019-VS2019":
|
||||
case "WINDOWS-2019":
|
||||
case "WINDOWS LATEST":
|
||||
case "WINDOWS-LATEST":
|
||||
return "Hosted Windows 2019 with VS2019";
|
||||
case "WINDOWS SERVER 1803":
|
||||
case "WIN1803":
|
||||
return "Hosted Windows Container";
|
||||
case "MACOS 10.13":
|
||||
case "MACOS-10.13":
|
||||
case "XCODE 9 ON MACOS 10.13":
|
||||
case "XCODE9-MACOS10.13":
|
||||
case "XCODE 10 ON MACOS 10.13":
|
||||
case "XCODE10-MACOS10.13":
|
||||
return "Hosted macOS High Sierra";
|
||||
case "MACOS 10.14":
|
||||
case "MACOS-10.14":
|
||||
case "MACOS LATEST":
|
||||
case "MACOS-LATEST":
|
||||
return "Hosted macOS";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PipelineBuildContexts have build options.
|
||||
/// GraphExecutionContexts have dependencies.
|
||||
/// We might need either depending on the situation.
|
||||
/// </summary>
|
||||
private TaskAgentPoolReference ValidateQueue(
|
||||
IPipelineContext context,
|
||||
ValidationResult result,
|
||||
BuildOptions buildOptions)
|
||||
{
|
||||
var queueId = 0;
|
||||
var queueName = (String)null;
|
||||
var queueNameIsUnresolvableExpression = false; // true iff Name is an expression, we're allowed to use them, and it has no current value
|
||||
var queue = this.Queue;
|
||||
if (queue != null)
|
||||
{
|
||||
queueId = queue.Id;
|
||||
|
||||
// resolve name
|
||||
var expressionValueName = queue.Name;
|
||||
if (expressionValueName != null && (buildOptions.EnableResourceExpressions || expressionValueName.IsLiteral))
|
||||
{
|
||||
// resolve expression
|
||||
try
|
||||
{
|
||||
queueName = expressionValueName.GetValue(context).Value;
|
||||
queueNameIsUnresolvableExpression = !expressionValueName.IsLiteral && String.IsNullOrEmpty(queueName);
|
||||
}
|
||||
catch (Exception ee)
|
||||
{
|
||||
// something bad happened trying to fetch the value.
|
||||
// We do not really care what though. Just record the error and move on.
|
||||
queueName = null;
|
||||
|
||||
if (buildOptions.ValidateExpressions && buildOptions.ValidateResources)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(ee.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// resolve name macro
|
||||
if (buildOptions.EnableResourceExpressions && queueName != null && VariableUtility.IsVariable(queueName))
|
||||
{
|
||||
queueName = context.ExpandVariables(queueName);
|
||||
if (VariableUtility.IsVariable(queueName))
|
||||
{
|
||||
// name appears to be a macro that is not defined.
|
||||
queueNameIsUnresolvableExpression = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (queueNameIsUnresolvableExpression || (queueId == 0 && String.IsNullOrEmpty(queueName)))
|
||||
{
|
||||
// could not determine what queue user was talking about
|
||||
if (!buildOptions.AllowEmptyQueueTarget && buildOptions.ValidateResources)
|
||||
{
|
||||
// expression-based queue names are allowed to be unresolved at compile time.
|
||||
// TEMPORARY: literal queue names do not error at compile time if special keys exist
|
||||
if (!queueNameIsUnresolvableExpression || buildOptions.ValidateExpressions)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(queueName))
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotFoundByName(queueName)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var expressionValueName = queue?.Name;
|
||||
if (expressionValueName == null || expressionValueName.IsLiteral)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotDefined()));
|
||||
}
|
||||
else if (expressionValueName != null)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotFoundByName(expressionValueName.Expression)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// we have a valid queue. record the reference
|
||||
result.AddQueueReference(id: queueId, name: queueName);
|
||||
|
||||
// Attempt to resolve the queue using any identifier specified. We will look up by either ID
|
||||
// or name and the ID is preferred since it is immutable and more specific.
|
||||
if (buildOptions.ValidateResources)
|
||||
{
|
||||
TaskAgentQueue taskAgentQueue = null;
|
||||
var resourceStore = context.ResourceStore;
|
||||
if (resourceStore != null)
|
||||
{
|
||||
if (queueId != 0)
|
||||
{
|
||||
taskAgentQueue = resourceStore.GetQueue(queueId);
|
||||
if (taskAgentQueue == null)
|
||||
{
|
||||
result.UnauthorizedResources.Queues.Add(new AgentQueueReference { Id = queueId });
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotFound(queueId)));
|
||||
}
|
||||
}
|
||||
else if (!String.IsNullOrEmpty(queueName))
|
||||
{
|
||||
taskAgentQueue = resourceStore.GetQueue(queueName);
|
||||
if (taskAgentQueue == null)
|
||||
{
|
||||
result.UnauthorizedResources.Queues.Add(new AgentQueueReference { Name = queueName });
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.QueueNotFoundByName(queueName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the resolved values inline to the resolved resource for this validation run
|
||||
if (taskAgentQueue != null)
|
||||
{
|
||||
this.Queue.Id = taskAgentQueue.Id;
|
||||
return taskAgentQueue.Pool;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal override void Validate(
|
||||
IPipelineContext context,
|
||||
BuildOptions buildOptions,
|
||||
ValidationResult result,
|
||||
IList<Step> steps,
|
||||
ISet<Demand> taskDemands)
|
||||
{
|
||||
// validate queue
|
||||
var resolvedPool = ValidateQueue(context, result, buildOptions);
|
||||
Boolean includeTaskDemands = resolvedPool == null || !resolvedPool.IsHosted;
|
||||
|
||||
// Add advanced-checkout min agent demand
|
||||
Boolean advancedCheckout = false;
|
||||
int checkoutTasks = 0;
|
||||
int injectedSystemTasks = 0;
|
||||
bool countInjectSystemTasks = true;
|
||||
for (int index = 0; index < steps.Count; index++)
|
||||
{
|
||||
var step = steps[index];
|
||||
// Task
|
||||
if (step.Type == StepType.Task)
|
||||
{
|
||||
var task = step as TaskStep;
|
||||
if (task.Name.StartsWith("__system_"))
|
||||
{
|
||||
if (countInjectSystemTasks)
|
||||
{
|
||||
injectedSystemTasks++;
|
||||
}
|
||||
}
|
||||
else if (task.IsCheckoutTask())
|
||||
{
|
||||
countInjectSystemTasks = false;
|
||||
checkoutTasks++;
|
||||
if (context.EnvironmentVersion < 2)
|
||||
{
|
||||
if (index > 0 && index - injectedSystemTasks > 0)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.CheckoutMustBeTheFirstStep()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (index > 0)
|
||||
{
|
||||
advancedCheckout = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (task.Inputs.TryGetValue(PipelineConstants.CheckoutTaskInputs.Repository, out String repository) &&
|
||||
!String.Equals(repository, PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase) &&
|
||||
!String.Equals(repository, PipelineConstants.NoneAlias, StringComparison.OrdinalIgnoreCase) &&
|
||||
!String.Equals(repository, PipelineConstants.DesignerRepo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.CheckoutStepRepositoryNotSupported(task.Inputs[PipelineConstants.CheckoutTaskInputs.Repository])));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
countInjectSystemTasks = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (checkoutTasks > 1)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.CheckoutMultipleRepositoryNotSupported()));
|
||||
}
|
||||
|
||||
if (advancedCheckout)
|
||||
{
|
||||
taskDemands.Add(new DemandMinimumVersion(PipelineConstants.AgentVersionDemandName, PipelineConstants.AdvancedCheckoutMinAgentVersion));
|
||||
}
|
||||
|
||||
// Now we need to ensure we have only a single demand for the mimimum agent version. We effectively remove
|
||||
// every agent version demand we find and keep track of the one with the highest value. Assuming we located
|
||||
// one or more of these demands we will ensure it is merged in at the end.
|
||||
var minimumAgentVersionDemand = ResolveAgentVersionDemand(taskDemands);
|
||||
minimumAgentVersionDemand = ResolveAgentVersionDemand(this.Demands, minimumAgentVersionDemand);
|
||||
|
||||
// not include demands from task if phase is running inside container
|
||||
// container suppose provide any required tool task needs
|
||||
if (this.Container != null)
|
||||
{
|
||||
includeTaskDemands = false;
|
||||
}
|
||||
|
||||
// Merge the phase demands with the implicit demands from tasks.
|
||||
if (includeTaskDemands && buildOptions.RollupStepDemands)
|
||||
{
|
||||
this.Demands.UnionWith(taskDemands);
|
||||
}
|
||||
|
||||
// If we resolved a minimum agent version demand then we go ahead and merge it in
|
||||
// We want to do this even if targetting Hosted
|
||||
if (minimumAgentVersionDemand != null)
|
||||
{
|
||||
this.Demands.Add(minimumAgentVersionDemand);
|
||||
}
|
||||
}
|
||||
|
||||
private static DemandMinimumVersion ResolveAgentVersionDemand(
|
||||
ISet<Demand> demands,
|
||||
DemandMinimumVersion currentMinimumVersion = null)
|
||||
{
|
||||
var minVersionDemand = DemandMinimumVersion.MaxAndRemove(demands);
|
||||
if (minVersionDemand != null && (currentMinimumVersion == null || DemandMinimumVersion.CompareVersion(minVersionDemand.Value, currentMinimumVersion.Value) > 0))
|
||||
{
|
||||
return minVersionDemand;
|
||||
}
|
||||
else
|
||||
{
|
||||
return currentMinimumVersion;
|
||||
}
|
||||
}
|
||||
|
||||
internal override JobExecutionContext CreateJobContext(
|
||||
PhaseExecutionContext context,
|
||||
String jobName,
|
||||
Int32 attempt,
|
||||
Boolean continueOnError,
|
||||
Int32 timeoutInMinutes,
|
||||
Int32 cancelTimeoutInMinutes,
|
||||
IJobFactory jobFactory)
|
||||
{
|
||||
context.Trace?.EnterProperty("CreateJobContext");
|
||||
var execution = this.Execution ?? new ParallelExecutionOptions();
|
||||
var jobContext = execution.CreateJobContext(
|
||||
context,
|
||||
jobName,
|
||||
attempt,
|
||||
this.Container,
|
||||
this.SidecarContainers,
|
||||
continueOnError,
|
||||
timeoutInMinutes,
|
||||
cancelTimeoutInMinutes,
|
||||
jobFactory);
|
||||
context.Trace?.LeaveProperty("CreateJobContext");
|
||||
|
||||
if (jobContext != null)
|
||||
{
|
||||
jobContext.Job.Definition.Workspace = this.Workspace?.Clone();
|
||||
}
|
||||
|
||||
return jobContext;
|
||||
}
|
||||
|
||||
internal override ExpandPhaseResult Expand(
|
||||
PhaseExecutionContext context,
|
||||
Boolean continueOnError,
|
||||
Int32 timeoutInMinutes,
|
||||
Int32 cancelTimeoutInMinutes,
|
||||
IJobFactory jobFactory,
|
||||
JobExpansionOptions options)
|
||||
{
|
||||
context.Trace?.EnterProperty("Expand");
|
||||
var execution = this.Execution ?? new ParallelExecutionOptions();
|
||||
var result = execution.Expand(
|
||||
context,
|
||||
this.Container,
|
||||
this.SidecarContainers,
|
||||
continueOnError,
|
||||
timeoutInMinutes,
|
||||
cancelTimeoutInMinutes,
|
||||
jobFactory,
|
||||
options);
|
||||
context.Trace?.LeaveProperty("Expand");
|
||||
|
||||
foreach (var job in result.Jobs)
|
||||
{
|
||||
job.Definition.Workspace = this.Workspace?.Clone();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_sidecarContainers?.Count == 0)
|
||||
{
|
||||
m_sidecarContainers = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "SidecarContainers", EmitDefaultValue = false)]
|
||||
private IDictionary<String, ExpressionValue<String>> m_sidecarContainers;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures conversion of a TaskAgentQueue into an AgentQueueReference works properly when the serializer
|
||||
/// is configured to write/honor type information. This is a temporary converter that may be removed after
|
||||
/// M127 ships.
|
||||
/// </summary>
|
||||
private sealed class QueueJsonConverter : VssSecureJsonConverter
|
||||
{
|
||||
public override Boolean CanWrite => false;
|
||||
|
||||
public override Boolean CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.Equals(typeof(AgentQueueReference));
|
||||
}
|
||||
|
||||
public override Object ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
Object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
var rawValue = JObject.Load(reader);
|
||||
using (var objectReader = rawValue.CreateReader())
|
||||
{
|
||||
var newValue = new AgentQueueReference();
|
||||
serializer.Populate(objectReader, newValue);
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(
|
||||
JsonWriter writer,
|
||||
Object value,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Sdk/DTPipelines/Pipelines/Artifacts/ArtifactConstants.cs
Normal file
15
src/Sdk/DTPipelines/Pipelines/Artifacts/ArtifactConstants.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Artifacts
|
||||
{
|
||||
public static class ArtifactConstants
|
||||
{
|
||||
internal static class ArtifactType
|
||||
{
|
||||
internal const String Build = nameof(Build);
|
||||
internal const String Container = nameof(Container);
|
||||
internal const String Package = nameof(Package);
|
||||
internal const String SourceControl = nameof(SourceControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.Pipelines.Artifacts;
|
||||
namespace GitHub.DistributedTask.Orchestration.Server.Artifacts
|
||||
{
|
||||
public static class DownloadStepExtensions
|
||||
{
|
||||
public static Boolean IsDownloadBuildStepExists(this IReadOnlyList<JobStep> steps)
|
||||
{
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (step is TaskStep taskStep)
|
||||
{
|
||||
if (taskStep.IsDownloadBuildTask())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Boolean IsDownloadBuildTask(this Step step)
|
||||
{
|
||||
if (step is TaskStep taskStep &&
|
||||
taskStep.Reference != null &&
|
||||
taskStep.Reference.Name.Equals(YamlArtifactConstants.DownloadBuild, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Boolean IsDownloadStepDisabled(this Step step)
|
||||
{
|
||||
// either download task or downloadBuild task has none keyword return true.
|
||||
if (step is TaskStep taskStep &&
|
||||
taskStep.Inputs.TryGetValue(PipelineArtifactConstants.DownloadTaskInputs.Alias, out String alias) &&
|
||||
String.Equals(alias, YamlArtifactConstants.None, StringComparison.OrdinalIgnoreCase) &&
|
||||
(step.IsDownloadBuildTask() || step.IsDownloadTask()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Boolean IsDownloadTask(this Step step)
|
||||
{
|
||||
if (step is TaskStep taskStep &&
|
||||
taskStep.Reference != null &&
|
||||
taskStep.Reference.Id.Equals(PipelineArtifactConstants.DownloadTask.Id) &&
|
||||
taskStep.Reference.Version == PipelineArtifactConstants.DownloadTask.Version)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static Boolean IsDownloadCurrentPipelineArtifactStep(this Step step)
|
||||
{
|
||||
if (step is TaskStep taskStep &&
|
||||
taskStep.IsDownloadTask() &&
|
||||
taskStep.Inputs.TryGetValue(PipelineArtifactConstants.DownloadTaskInputs.Alias, out String alias) &&
|
||||
String.Equals(alias, YamlArtifactConstants.Current, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Boolean IsDownloadPipelineArtifactStepDisabled(this TaskStep step)
|
||||
{
|
||||
if (step.IsDownloadTask() &&
|
||||
step.Inputs.TryGetValue(PipelineArtifactConstants.DownloadTaskInputs.Alias, out String alias) &&
|
||||
String.Equals(alias, YamlArtifactConstants.None, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Boolean IsDownloadExternalPipelineArtifactStep(this TaskStep step)
|
||||
{
|
||||
if (step.IsDownloadTask() &&
|
||||
step.Inputs != null &&
|
||||
step.Inputs.TryGetValue(PipelineArtifactConstants.DownloadTaskInputs.Alias, out String alias) &&
|
||||
!String.IsNullOrEmpty(alias) &&
|
||||
!alias.Equals(YamlArtifactConstants.Current, StringComparison.OrdinalIgnoreCase) &&
|
||||
!alias.Equals(YamlArtifactConstants.None, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String GetAliasFromTaskStep(this TaskStep step)
|
||||
{
|
||||
return step.Inputs.TryGetValue(PipelineArtifactConstants.DownloadTaskInputs.Alias, out String alias)
|
||||
? alias
|
||||
: String.Empty;
|
||||
}
|
||||
|
||||
public static Boolean IsDownloadPipelineArtifactStepExists(this IReadOnlyList<JobStep> steps)
|
||||
{
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (step is TaskStep taskStep)
|
||||
{
|
||||
if (taskStep.IsDownloadTask())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void Merge(
|
||||
this IDictionary<String, String> first,
|
||||
IDictionary<String, String> second)
|
||||
{
|
||||
foreach (var key in second?.Keys ?? new List<String>())
|
||||
{
|
||||
first[key] = second[key];
|
||||
}
|
||||
}
|
||||
|
||||
public static void Merge(
|
||||
this IDictionary<String, String> first,
|
||||
IReadOnlyDictionary<String, String> second)
|
||||
{
|
||||
foreach (var key in second?.Keys ?? new List<String>())
|
||||
{
|
||||
first[key] = second[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/Sdk/DTPipelines/Pipelines/Artifacts/IArtifactResolver.cs
Normal file
49
src/Sdk/DTPipelines/Pipelines/Artifacts/IArtifactResolver.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Artifacts
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism to resolve the artifacts
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IArtifactResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Given a resource, it gets the corresponding task id from its extension
|
||||
/// </summary>
|
||||
/// <param name="resource"></param>
|
||||
/// <returns></returns>
|
||||
Guid GetArtifactDownloadTaskId(Resource resource);
|
||||
|
||||
/// <summary>
|
||||
/// Given a resource and step, it maps the resource properties to task inputs
|
||||
/// </summary>
|
||||
/// <param name="resource"></param>
|
||||
/// <param name="taskStep"></param>
|
||||
void PopulateMappedTaskInputs(Resource resource, TaskStep taskStep);
|
||||
|
||||
/// <summary>
|
||||
/// Given an artifact step, it resolves the artifact and returns a download artifact task
|
||||
/// </summary>
|
||||
/// <param name="pipelineContext"></param>
|
||||
/// <param name="step"></param>
|
||||
/// <returns></returns>
|
||||
Boolean ResolveStep(IPipelineContext pipelineContext, JobStep step, out IList<TaskStep> resolvedSteps);
|
||||
|
||||
/// <summary>
|
||||
/// Given resource store and task step it translate the taskStep into actual task reference with mapped inputs
|
||||
/// </summary>
|
||||
/// <param name="resourceStore"></param>
|
||||
/// <param name="taskStep"></param>
|
||||
/// <returns></returns>
|
||||
Boolean ResolveStep(IResourceStore resourceStore, TaskStep taskStep, out String errorMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Validate the given resource in the YAML file. Also resolve version for the resource if not resolved already
|
||||
/// </summary>
|
||||
/// <param name="resources"></param>
|
||||
Boolean ValidateDeclaredResource(Resource resource, out PipelineValidationError error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Artifacts
|
||||
{
|
||||
public static class PipelineArtifactConstants
|
||||
{
|
||||
internal static class CommonArtifactTaskInputValues
|
||||
{
|
||||
internal const String DefaultDownloadPath = "$(Pipeline.Workspace)";
|
||||
internal const String DefaultDownloadPattern = "**";
|
||||
}
|
||||
|
||||
public static class PipelineArtifactTaskInputs
|
||||
{
|
||||
public const String ArtifactName = "artifactName";
|
||||
|
||||
public const String BuildType = "buildType";
|
||||
|
||||
public const String BuildId = "buildId";
|
||||
|
||||
public const String BuildVersionToDownload = "buildVersionToDownload";
|
||||
|
||||
public const String Definition = "definition";
|
||||
|
||||
public const String DownloadType = "downloadType";
|
||||
|
||||
public const String DownloadPath = "downloadPath";
|
||||
|
||||
public const String FileSharePath = "fileSharePath";
|
||||
|
||||
public const String ItemPattern = "itemPattern";
|
||||
|
||||
public const String Project = "project";
|
||||
}
|
||||
|
||||
public static class PipelineArtifactTaskInputValues
|
||||
{
|
||||
public const String DownloadTypeSingle = "single";
|
||||
public const String SpecificBuildType = "specific";
|
||||
public const String CurrentBuildType = "current";
|
||||
public const String AutomaticMode = "automatic";
|
||||
public const String ManualMode = "manual";
|
||||
}
|
||||
|
||||
internal static class YamlConstants
|
||||
{
|
||||
internal const String Connection = "connection";
|
||||
internal const String Current = "current";
|
||||
internal const String None = "none";
|
||||
}
|
||||
|
||||
public static class ArtifactTypes
|
||||
{
|
||||
public const string AzurePipelineArtifactType = "Pipeline";
|
||||
}
|
||||
|
||||
public static class DownloadTaskInputs
|
||||
{
|
||||
public const String Alias = "alias";
|
||||
public const String Artifact = "artifact";
|
||||
public const String Mode = "mode";
|
||||
public const String Path = "path";
|
||||
public const String Patterns = "patterns";
|
||||
}
|
||||
|
||||
public static class TraceConstants
|
||||
{
|
||||
public const String Area = "PipelineArtifacts";
|
||||
public const String DownloadPipelineArtifactFeature = "DownloadPipelineArtifact";
|
||||
}
|
||||
|
||||
public static readonly TaskDefinition DownloadTask = new TaskDefinition
|
||||
{
|
||||
Id = new Guid("30f35852-3f7e-4c0c-9a88-e127b4f97211"),
|
||||
Name = "Download",
|
||||
FriendlyName = "Download Artifact",
|
||||
Author = "Microsoft",
|
||||
RunsOn = { TaskRunsOnConstants.RunsOnAgent },
|
||||
Version = new TaskVersion("1.0.0"),
|
||||
Description = "Downloads pipeline type artifacts.",
|
||||
HelpMarkDown = "[More Information](https://github.com)",
|
||||
Inputs = {
|
||||
new TaskInputDefinition()
|
||||
{
|
||||
Name = DownloadTaskInputs.Artifact,
|
||||
Required = true,
|
||||
InputType = TaskInputType.String
|
||||
},
|
||||
new TaskInputDefinition()
|
||||
{
|
||||
Name = DownloadTaskInputs.Patterns,
|
||||
Required = false,
|
||||
DefaultValue = "**",
|
||||
InputType = TaskInputType.String
|
||||
},
|
||||
new TaskInputDefinition()
|
||||
{
|
||||
Name = DownloadTaskInputs.Path,
|
||||
Required = false,
|
||||
InputType = TaskInputType.String
|
||||
},
|
||||
new TaskInputDefinition()
|
||||
{
|
||||
Name=DownloadTaskInputs.Alias,
|
||||
Required = false,
|
||||
InputType = TaskInputType.String
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Artifacts
|
||||
{
|
||||
public static class YamlArtifactConstants
|
||||
{
|
||||
public const String Alias = "alias";
|
||||
public const String Connection = "connection";
|
||||
public const String Current = "current";
|
||||
public const String Download = "download";
|
||||
public const String DownloadBuild = "downloadBuild";
|
||||
public const String None = "none";
|
||||
public const String Path = "path";
|
||||
public const String Patterns = "patterns";
|
||||
}
|
||||
}
|
||||
119
src/Sdk/DTPipelines/Pipelines/BuildOptions.cs
Normal file
119
src/Sdk/DTPipelines/Pipelines/BuildOptions.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism for controlling validation behaviors.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class BuildOptions
|
||||
{
|
||||
public static BuildOptions None { get; } = new BuildOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not a queue target without a queue should be considered an
|
||||
/// error.
|
||||
/// </summary>
|
||||
public Boolean AllowEmptyQueueTarget
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allow hyphens in names checked by the NameValidator. Used for yaml workflow schema
|
||||
/// </summary>
|
||||
public Boolean AllowHyphenNames
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to demand the latest agent version.
|
||||
/// </summary>
|
||||
public Boolean DemandLatestAgent
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, resource definitions are allowed to use expressions
|
||||
/// </summary>
|
||||
public Boolean EnableResourceExpressions
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not to resolve resource version.
|
||||
/// </summary>
|
||||
public Boolean ResolveResourceVersions
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether input aliases defined in a task definition are honored.
|
||||
/// </summary>
|
||||
public Boolean ResolveTaskInputAliases
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the individual step demands should be rolled up into their
|
||||
/// parent phase's demands. Settings this value to true will result in Phase's demand sets being a superset
|
||||
/// of their children's demands.
|
||||
/// </summary>
|
||||
public Boolean RollupStepDemands
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, all expressions must be resolvable given a provided context.
|
||||
/// This is normally going to be false for plan compile time and true for plan runtime.
|
||||
/// </summary>
|
||||
public Boolean ValidateExpressions
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not to validate resource existence and other constraints.
|
||||
/// </summary>
|
||||
public Boolean ValidateResources
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not step names provided by the caller should be validated for
|
||||
/// correctness and uniqueness. Setting this value to false will automatically fix invalid step names and
|
||||
/// de-duplicate step names which may lead to unexpected behavior at runtime when binding output variables.
|
||||
/// </summary>
|
||||
public Boolean ValidateStepNames
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not to run input validation defined by the task author.
|
||||
/// </summary>
|
||||
public Boolean ValidateTaskInputs
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Sdk/DTPipelines/Pipelines/BuildResource.cs
Normal file
68
src/Sdk/DTPipelines/Pipelines/BuildResource.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class BuildPropertyNames
|
||||
{
|
||||
public static readonly String Branch = "branch";
|
||||
public static readonly String Connection = "connection";
|
||||
public static readonly String Source = "source";
|
||||
public static readonly String Type = "type";
|
||||
public static readonly String Version = "version";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a data contract for a build resource referenced by a pipeline.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class BuildResource : Resource
|
||||
{
|
||||
public BuildResource()
|
||||
{
|
||||
}
|
||||
|
||||
protected BuildResource(BuildResource resourceToCopy)
|
||||
: base(resourceToCopy)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of build resource.
|
||||
/// </summary>
|
||||
public String Type
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Properties.Get<String>(BuildPropertyNames.Type);
|
||||
}
|
||||
set
|
||||
{
|
||||
this.Properties.Set(BuildPropertyNames.Type, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the version of the build resource.
|
||||
/// </summary>
|
||||
public String Version
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Properties.Get<String>(BuildPropertyNames.Version);
|
||||
}
|
||||
set
|
||||
{
|
||||
this.Properties.Set(BuildPropertyNames.Version, value);
|
||||
}
|
||||
}
|
||||
|
||||
public BuildResource Clone()
|
||||
{
|
||||
return new BuildResource(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Checkpoints
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
public class CheckpointContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique id of the checkpoint, also used as the timeline record id
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Auth token for querying DistributedTask
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public String Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint Instance Id
|
||||
/// Use this for sending decision events and tracing telemetry.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public String OrchestrationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// PlanId
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public Guid PlanId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which TaskHub to use when sending decision events;
|
||||
/// Use this for sending decision events.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public String HubName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The project requesting decision.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public CheckpointScope Project { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The pipeline (definition) requesting decision.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public PipelineScope Pipeline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The graph node requesting decision.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public GraphNodeScope GraphNode { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Checkpoints
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
public class CheckpointDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Checkpoint id, provided on context
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Decision
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public String Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional information (optional)
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = false, EmitDefaultValue = false)]
|
||||
public String Message { get; set; }
|
||||
|
||||
// Decision possibilities
|
||||
public const String Approved = "Approved";
|
||||
public const String Denied = "Denied";
|
||||
public const String Canceled = "Canceled";
|
||||
}
|
||||
}
|
||||
56
src/Sdk/DTPipelines/Pipelines/Checkpoints/CheckpointScope.cs
Normal file
56
src/Sdk/DTPipelines/Pipelines/Checkpoints/CheckpointScope.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Checkpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides context regarding the state of the orchestration.
|
||||
/// Consumers may choose to use this information to cache decisions.
|
||||
/// EG, if you wanted to return the same decision for this and all
|
||||
/// future requests issuing from the same project / pipeline / stage / run
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
public class CheckpointScope
|
||||
{
|
||||
/// <summary>
|
||||
/// May be used in uniquely identify this scope for future reference.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public String Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The friendly name of the scope
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String Name { get; set; }
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
public class GraphNodeScope : CheckpointScope
|
||||
{
|
||||
/// <summary>
|
||||
/// Facilitates approving only a single attempt of a graph node in a specific run of a pipeline.
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public Int32 Attempt { get; set; } = 1;
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
public class PipelineScope : CheckpointScope
|
||||
{
|
||||
/// <summary>
|
||||
/// Pipeline URLs
|
||||
/// </summary>
|
||||
[DataMember(IsRequired = true)]
|
||||
public TaskOrchestrationOwner Owner { get; set; }
|
||||
}
|
||||
}
|
||||
22
src/Sdk/DTPipelines/Pipelines/Checkpoints/ResourceInfo.cs
Normal file
22
src/Sdk/DTPipelines/Pipelines/Checkpoints/ResourceInfo.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Checkpoints
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
public class ResourceInfo
|
||||
{
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String Id { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String Name { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String TypeName { get; set; }
|
||||
}
|
||||
}
|
||||
25
src/Sdk/DTPipelines/Pipelines/ConditionResult.cs
Normal file
25
src/Sdk/DTPipelines/Pipelines/ConditionResult.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class ConditionResult
|
||||
{
|
||||
[DataMember]
|
||||
public Boolean Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String Trace
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/Sdk/DTPipelines/Pipelines/ContainerResource.cs
Normal file
113
src/Sdk/DTPipelines/Pipelines/ContainerResource.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class ContainerPropertyNames
|
||||
{
|
||||
public const String Env = "env";
|
||||
public const String Image = "image";
|
||||
public const String Options = "options";
|
||||
public const String Volumes = "volumes";
|
||||
public const String Ports = "ports";
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class ContainerResource : Resource
|
||||
{
|
||||
[JsonConstructor]
|
||||
public ContainerResource()
|
||||
{
|
||||
}
|
||||
|
||||
private ContainerResource(ContainerResource referenceToCopy)
|
||||
: base(referenceToCopy)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the environment which is provided to the container.
|
||||
/// </summary>
|
||||
public IDictionary<String, String> Environment
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Properties.Get<IDictionary<String, String>>(ContainerPropertyNames.Env);
|
||||
}
|
||||
set
|
||||
{
|
||||
this.Properties.Set(ContainerPropertyNames.Env, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the container image name.
|
||||
/// </summary>
|
||||
public String Image
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Properties.Get<String>(ContainerPropertyNames.Image);
|
||||
}
|
||||
set
|
||||
{
|
||||
this.Properties.Set(ContainerPropertyNames.Image, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the options used for the container instance.
|
||||
/// </summary>
|
||||
public String Options
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Properties.Get<String>(ContainerPropertyNames.Options);
|
||||
}
|
||||
set
|
||||
{
|
||||
this.Properties.Set(ContainerPropertyNames.Options, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the volumes which are mounted into the container.
|
||||
/// </summary>
|
||||
public IList<String> Volumes
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Properties.Get<IList<String>>(ContainerPropertyNames.Volumes);
|
||||
}
|
||||
set
|
||||
{
|
||||
this.Properties.Set(ContainerPropertyNames.Volumes, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ports which are exposed on the container.
|
||||
/// </summary>
|
||||
public IList<String> Ports
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Properties.Get<IList<String>>(ContainerPropertyNames.Ports);
|
||||
}
|
||||
set
|
||||
{
|
||||
this.Properties.Set(ContainerPropertyNames.Ports, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ContainerResource Clone()
|
||||
{
|
||||
return new ContainerResource(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/Sdk/DTPipelines/Pipelines/ContextData/ArrayContextData.cs
Normal file
113
src/Sdk/DTPipelines/Pipelines/ContextData/ArrayContextData.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[DataContract]
|
||||
[JsonObject]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class ArrayContextData : PipelineContextData, IEnumerable<PipelineContextData>, IReadOnlyArray
|
||||
{
|
||||
public ArrayContextData()
|
||||
: base(PipelineContextDataType.Array)
|
||||
{
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public Int32 Count => m_items?.Count ?? 0;
|
||||
|
||||
public PipelineContextData this[Int32 index] => m_items[index];
|
||||
|
||||
Object IReadOnlyArray.this[Int32 index] => m_items[index];
|
||||
|
||||
public void Add(PipelineContextData item)
|
||||
{
|
||||
if (m_items == null)
|
||||
{
|
||||
m_items = new List<PipelineContextData>();
|
||||
}
|
||||
|
||||
m_items.Add(item);
|
||||
}
|
||||
|
||||
public override PipelineContextData Clone()
|
||||
{
|
||||
var result = new ArrayContextData();
|
||||
if (m_items?.Count > 0)
|
||||
{
|
||||
result.m_items = new List<PipelineContextData>(m_items.Count);
|
||||
foreach (var item in m_items)
|
||||
{
|
||||
result.m_items.Add(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override JToken ToJToken()
|
||||
{
|
||||
var result = new JArray();
|
||||
if (m_items?.Count > 0)
|
||||
{
|
||||
foreach (var item in m_items)
|
||||
{
|
||||
result.Add(item?.ToJToken() ?? JValue.CreateNull());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerator<PipelineContextData> GetEnumerator()
|
||||
{
|
||||
if (m_items?.Count > 0)
|
||||
{
|
||||
foreach (var item in m_items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
if (m_items?.Count > 0)
|
||||
{
|
||||
foreach (var item in m_items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IReadOnlyArray.GetEnumerator()
|
||||
{
|
||||
if (m_items?.Count > 0)
|
||||
{
|
||||
foreach (var item in m_items)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_items?.Count == 0)
|
||||
{
|
||||
m_items = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "a", EmitDefaultValue = false)]
|
||||
private List<PipelineContextData> m_items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class BooleanContextData : PipelineContextData, IBoolean
|
||||
{
|
||||
public BooleanContextData(Boolean value)
|
||||
: base(PipelineContextDataType.Boolean)
|
||||
{
|
||||
m_value = value;
|
||||
}
|
||||
|
||||
public Boolean Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
}
|
||||
|
||||
public override PipelineContextData Clone()
|
||||
{
|
||||
return new BooleanContextData(m_value);
|
||||
}
|
||||
|
||||
public override JToken ToJToken()
|
||||
{
|
||||
return (JToken)m_value;
|
||||
}
|
||||
|
||||
public override String ToString()
|
||||
{
|
||||
return m_value ? "true" : "false";
|
||||
}
|
||||
|
||||
Boolean IBoolean.GetBoolean()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
public static implicit operator Boolean(BooleanContextData data)
|
||||
{
|
||||
return data.Value;
|
||||
}
|
||||
|
||||
public static implicit operator BooleanContextData(Boolean data)
|
||||
{
|
||||
return new BooleanContextData(data);
|
||||
}
|
||||
|
||||
[DataMember(Name = "b", EmitDefaultValue = false)]
|
||||
private Boolean m_value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[DataContract]
|
||||
[JsonObject]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class CaseSensitiveDictionaryContextData : PipelineContextData, IEnumerable<KeyValuePair<String, PipelineContextData>>, IReadOnlyObject
|
||||
{
|
||||
public CaseSensitiveDictionaryContextData()
|
||||
: base(PipelineContextDataType.CaseSensitiveDictionary)
|
||||
{
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public Int32 Count => m_list?.Count ?? 0;
|
||||
|
||||
[IgnoreDataMember]
|
||||
public IEnumerable<String> Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return pair.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public IEnumerable<PipelineContextData> Values
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<Object> IReadOnlyObject.Values
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<String, Int32> IndexLookup
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_indexLookup == null)
|
||||
{
|
||||
m_indexLookup = new Dictionary<String, Int32>(StringComparer.Ordinal);
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
for (var i = 0; i < m_list.Count; i++)
|
||||
{
|
||||
var pair = m_list[i];
|
||||
m_indexLookup.Add(pair.Key, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m_indexLookup;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DictionaryContextDataPair> List
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list == null)
|
||||
{
|
||||
m_list = new List<DictionaryContextDataPair>();
|
||||
}
|
||||
|
||||
return m_list;
|
||||
}
|
||||
}
|
||||
|
||||
public PipelineContextData this[String key]
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = IndexLookup[key];
|
||||
return m_list[index].Value;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
// Existing
|
||||
if (IndexLookup.TryGetValue(key, out var index))
|
||||
{
|
||||
key = m_list[index].Key; // preserve casing
|
||||
m_list[index] = new DictionaryContextDataPair(key, value);
|
||||
}
|
||||
// New
|
||||
else
|
||||
{
|
||||
Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object IReadOnlyObject.this[String key]
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = IndexLookup[key];
|
||||
return m_list[index].Value;
|
||||
}
|
||||
}
|
||||
|
||||
internal KeyValuePair<String, PipelineContextData> this[Int32 index]
|
||||
{
|
||||
get
|
||||
{
|
||||
var pair = m_list[index];
|
||||
return new KeyValuePair<String, PipelineContextData>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(IEnumerable<KeyValuePair<String, PipelineContextData>> pairs)
|
||||
{
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(
|
||||
String key,
|
||||
PipelineContextData value)
|
||||
{
|
||||
IndexLookup.Add(key, m_list?.Count ?? 0);
|
||||
List.Add(new DictionaryContextDataPair(key, value));
|
||||
}
|
||||
|
||||
public override PipelineContextData Clone()
|
||||
{
|
||||
var result = new CaseSensitiveDictionaryContextData();
|
||||
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
result.m_list = new List<DictionaryContextDataPair>(m_list.Count);
|
||||
foreach (var item in m_list)
|
||||
{
|
||||
result.m_list.Add(new DictionaryContextDataPair(item.Key, item.Value?.Clone()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override JToken ToJToken()
|
||||
{
|
||||
var json = new JObject();
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var item in m_list)
|
||||
{
|
||||
json.Add(item.Key, item.Value?.ToJToken() ?? JValue.CreateNull());
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
public Boolean ContainsKey(String key)
|
||||
{
|
||||
return TryGetValue(key, out _);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<String, PipelineContextData>> GetEnumerator()
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return new KeyValuePair<String, PipelineContextData>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return new KeyValuePair<String, PipelineContextData>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IReadOnlyObject.GetEnumerator()
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return new KeyValuePair<String, Object>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean TryGetValue(
|
||||
String key,
|
||||
out PipelineContextData value)
|
||||
{
|
||||
if (m_list?.Count > 0 &&
|
||||
IndexLookup.TryGetValue(key, out var index))
|
||||
{
|
||||
value = m_list[index].Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean IReadOnlyObject.TryGetValue(
|
||||
String key,
|
||||
out Object value)
|
||||
{
|
||||
if (TryGetValue(key, out PipelineContextData data))
|
||||
{
|
||||
value = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_list?.Count == 0)
|
||||
{
|
||||
m_list = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
private sealed class DictionaryContextDataPair
|
||||
{
|
||||
public DictionaryContextDataPair(
|
||||
String key,
|
||||
PipelineContextData value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
[DataMember(Name = "k")]
|
||||
public readonly String Key;
|
||||
|
||||
[DataMember(Name = "v")]
|
||||
public readonly PipelineContextData Value;
|
||||
}
|
||||
|
||||
private Dictionary<String, Int32> m_indexLookup;
|
||||
|
||||
[DataMember(Name = "d", EmitDefaultValue = false)]
|
||||
private List<DictionaryContextDataPair> m_list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[DataContract]
|
||||
[JsonObject]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class DictionaryContextData : PipelineContextData, IEnumerable<KeyValuePair<String, PipelineContextData>>, IReadOnlyObject
|
||||
{
|
||||
public DictionaryContextData()
|
||||
: base(PipelineContextDataType.Dictionary)
|
||||
{
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public Int32 Count => m_list?.Count ?? 0;
|
||||
|
||||
[IgnoreDataMember]
|
||||
public IEnumerable<String> Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return pair.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public IEnumerable<PipelineContextData> Values
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<Object> IReadOnlyObject.Values
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<String, Int32> IndexLookup
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_indexLookup == null)
|
||||
{
|
||||
m_indexLookup = new Dictionary<String, Int32>(StringComparer.OrdinalIgnoreCase);
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
for (var i = 0; i < m_list.Count; i++)
|
||||
{
|
||||
var pair = m_list[i];
|
||||
m_indexLookup.Add(pair.Key, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m_indexLookup;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DictionaryContextDataPair> List
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_list == null)
|
||||
{
|
||||
m_list = new List<DictionaryContextDataPair>();
|
||||
}
|
||||
|
||||
return m_list;
|
||||
}
|
||||
}
|
||||
|
||||
public PipelineContextData this[String key]
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = IndexLookup[key];
|
||||
return m_list[index].Value;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
// Existing
|
||||
if (IndexLookup.TryGetValue(key, out var index))
|
||||
{
|
||||
key = m_list[index].Key; // preserve casing
|
||||
m_list[index] = new DictionaryContextDataPair(key, value);
|
||||
}
|
||||
// New
|
||||
else
|
||||
{
|
||||
Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object IReadOnlyObject.this[String key]
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = IndexLookup[key];
|
||||
return m_list[index].Value;
|
||||
}
|
||||
}
|
||||
|
||||
internal KeyValuePair<String, PipelineContextData> this[Int32 index]
|
||||
{
|
||||
get
|
||||
{
|
||||
var pair = m_list[index];
|
||||
return new KeyValuePair<String, PipelineContextData>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(IEnumerable<KeyValuePair<String, PipelineContextData>> pairs)
|
||||
{
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(
|
||||
String key,
|
||||
PipelineContextData value)
|
||||
{
|
||||
IndexLookup.Add(key, m_list?.Count ?? 0);
|
||||
List.Add(new DictionaryContextDataPair(key, value));
|
||||
}
|
||||
|
||||
public override PipelineContextData Clone()
|
||||
{
|
||||
var result = new DictionaryContextData();
|
||||
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
result.m_list = new List<DictionaryContextDataPair>(m_list.Count);
|
||||
foreach (var item in m_list)
|
||||
{
|
||||
result.m_list.Add(new DictionaryContextDataPair(item.Key, item.Value?.Clone()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override JToken ToJToken()
|
||||
{
|
||||
var json = new JObject();
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var item in m_list)
|
||||
{
|
||||
json.Add(item.Key, item.Value?.ToJToken() ?? JValue.CreateNull());
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
public Boolean ContainsKey(String key)
|
||||
{
|
||||
return TryGetValue(key, out _);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<String, PipelineContextData>> GetEnumerator()
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return new KeyValuePair<String, PipelineContextData>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return new KeyValuePair<String, PipelineContextData>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IReadOnlyObject.GetEnumerator()
|
||||
{
|
||||
if (m_list?.Count > 0)
|
||||
{
|
||||
foreach (var pair in m_list)
|
||||
{
|
||||
yield return new KeyValuePair<String, Object>(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean TryGetValue(
|
||||
String key,
|
||||
out PipelineContextData value)
|
||||
{
|
||||
if (m_list?.Count > 0 &&
|
||||
IndexLookup.TryGetValue(key, out var index))
|
||||
{
|
||||
value = m_list[index].Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean IReadOnlyObject.TryGetValue(
|
||||
String key,
|
||||
out Object value)
|
||||
{
|
||||
if (TryGetValue(key, out PipelineContextData data))
|
||||
{
|
||||
value = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_list?.Count == 0)
|
||||
{
|
||||
m_list = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
private sealed class DictionaryContextDataPair
|
||||
{
|
||||
public DictionaryContextDataPair(
|
||||
String key,
|
||||
PipelineContextData value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
[DataMember(Name = "k")]
|
||||
public readonly String Key;
|
||||
|
||||
[DataMember(Name = "v")]
|
||||
public readonly PipelineContextData Value;
|
||||
}
|
||||
|
||||
private Dictionary<String, Int32> m_indexLookup;
|
||||
|
||||
[DataMember(Name = "d", EmitDefaultValue = false)]
|
||||
private List<DictionaryContextDataPair> m_list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class JTokenExtensions
|
||||
{
|
||||
public static PipelineContextData ToPipelineContextData(this JToken value)
|
||||
{
|
||||
return value.ToPipelineContextData(1, 100);
|
||||
}
|
||||
|
||||
public static PipelineContextData ToPipelineContextData(
|
||||
this JToken value,
|
||||
Int32 depth,
|
||||
Int32 maxDepth)
|
||||
{
|
||||
if (depth < maxDepth)
|
||||
{
|
||||
if (value.Type == JTokenType.String)
|
||||
{
|
||||
return new StringContextData((String)value);
|
||||
}
|
||||
else if (value.Type == JTokenType.Boolean)
|
||||
{
|
||||
return new BooleanContextData((Boolean)value);
|
||||
}
|
||||
else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
|
||||
{
|
||||
return new NumberContextData((Double)value);
|
||||
}
|
||||
else if (value.Type == JTokenType.Object)
|
||||
{
|
||||
var subContext = new DictionaryContextData();
|
||||
var obj = (JObject)value;
|
||||
foreach (var property in obj.Properties())
|
||||
{
|
||||
subContext[property.Name] = ToPipelineContextData(property.Value, depth + 1, maxDepth);
|
||||
}
|
||||
return subContext;
|
||||
}
|
||||
else if (value.Type == JTokenType.Array)
|
||||
{
|
||||
var arrayContext = new ArrayContextData();
|
||||
var arr = (JArray)value;
|
||||
foreach (var element in arr)
|
||||
{
|
||||
arrayContext.Add(ToPipelineContextData(element, depth + 1, maxDepth));
|
||||
}
|
||||
return arrayContext;
|
||||
}
|
||||
else if (value.Type == JTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't understand the type or have reached our max, return as string
|
||||
return new StringContextData(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class NumberContextData : PipelineContextData, INumber
|
||||
{
|
||||
public NumberContextData(Double value)
|
||||
: base(PipelineContextDataType.Number)
|
||||
{
|
||||
m_value = value;
|
||||
}
|
||||
|
||||
public Double Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
}
|
||||
|
||||
public override PipelineContextData Clone()
|
||||
{
|
||||
return new NumberContextData(m_value);
|
||||
}
|
||||
|
||||
public override JToken ToJToken()
|
||||
{
|
||||
if (Double.IsNaN(m_value) || m_value == Double.PositiveInfinity || m_value == Double.NegativeInfinity)
|
||||
{
|
||||
return (JToken)m_value;
|
||||
}
|
||||
|
||||
var floored = Math.Floor(m_value);
|
||||
if (m_value == floored && m_value <= (Double)Int32.MaxValue && m_value >= (Double)Int32.MinValue)
|
||||
{
|
||||
Int32 flooredInt = (Int32)floored;
|
||||
return (JToken)flooredInt;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (JToken)m_value;
|
||||
}
|
||||
}
|
||||
|
||||
public override String ToString()
|
||||
{
|
||||
return m_value.ToString("G15", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
Double INumber.GetNumber()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
public static implicit operator Double(NumberContextData data)
|
||||
{
|
||||
return data.Value;
|
||||
}
|
||||
|
||||
public static implicit operator NumberContextData(Double data)
|
||||
{
|
||||
return new NumberContextData(data);
|
||||
}
|
||||
|
||||
[DataMember(Name = "n", EmitDefaultValue = false)]
|
||||
private Double m_value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all template tokens
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
[JsonConverter(typeof(PipelineContextDataJsonConverter))]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public abstract class PipelineContextData
|
||||
{
|
||||
protected PipelineContextData(Int32 type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
[DataMember(Name = "t", EmitDefaultValue = false)]
|
||||
internal Int32 Type { get; }
|
||||
|
||||
public abstract PipelineContextData Clone();
|
||||
|
||||
public abstract JToken ToJToken();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class PipelineContextDataExtensions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static ArrayContextData AssertArray(
|
||||
this PipelineContextData value,
|
||||
String objectDescription)
|
||||
{
|
||||
if (value is ArrayContextData array)
|
||||
{
|
||||
return array;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(ArrayContextData)}' was expected.");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static DictionaryContextData AssertDictionary(
|
||||
this PipelineContextData value,
|
||||
String objectDescription)
|
||||
{
|
||||
if (value is DictionaryContextData dictionary)
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(DictionaryContextData)}' was expected.");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static StringContextData AssertString(
|
||||
this PipelineContextData value,
|
||||
String objectDescription)
|
||||
{
|
||||
if (value is StringContextData str)
|
||||
{
|
||||
return str;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(StringContextData)}' was expected.");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static BooleanContextData AssertBoolean(
|
||||
this PipelineContextData value,
|
||||
String objectDescription)
|
||||
{
|
||||
if (value is BooleanContextData boolValue)
|
||||
{
|
||||
return boolValue;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(BooleanContextData)}' was expected.");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static NumberContextData AssertNumber(
|
||||
this PipelineContextData value,
|
||||
String objectDescription)
|
||||
{
|
||||
if (value is NumberContextData num)
|
||||
{
|
||||
return num;
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(NumberContextData)}' was expected.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all context data objects (depth first)
|
||||
/// </summary>
|
||||
internal static IEnumerable<PipelineContextData> Traverse(this PipelineContextData value)
|
||||
{
|
||||
return Traverse(value, omitKeys: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all context data objects (depth first)
|
||||
/// </summary>
|
||||
internal static IEnumerable<PipelineContextData> Traverse(
|
||||
this PipelineContextData value,
|
||||
Boolean omitKeys)
|
||||
{
|
||||
yield return value;
|
||||
|
||||
if (value is ArrayContextData || value is DictionaryContextData)
|
||||
{
|
||||
var state = new TraversalState(null, value);
|
||||
while (state != null)
|
||||
{
|
||||
if (state.MoveNext(omitKeys))
|
||||
{
|
||||
value = state.Current;
|
||||
yield return value;
|
||||
|
||||
if (value is ArrayContextData || value is DictionaryContextData)
|
||||
{
|
||||
state = new TraversalState(state, value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
state = state.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static JToken ToJToken(this PipelineContextData value)
|
||||
{
|
||||
JToken result;
|
||||
|
||||
if (value is StringContextData str)
|
||||
{
|
||||
result = str.Value ?? String.Empty;
|
||||
}
|
||||
else if (value is BooleanContextData booleanValue)
|
||||
{
|
||||
result = booleanValue.Value;
|
||||
}
|
||||
else if (value is NumberContextData num)
|
||||
{
|
||||
result = num.Value;
|
||||
}
|
||||
else if (value is ArrayContextData array)
|
||||
{
|
||||
var jarray = new JArray();
|
||||
|
||||
foreach (var item in array)
|
||||
{
|
||||
jarray.Add(item.ToJToken()); // Recurse
|
||||
}
|
||||
|
||||
result = jarray;
|
||||
}
|
||||
else if (value is DictionaryContextData dictionary)
|
||||
{
|
||||
var jobject = new JObject();
|
||||
|
||||
foreach (var pair in dictionary)
|
||||
{
|
||||
var key = pair.Key ?? String.Empty;
|
||||
var value2 = pair.Value.ToJToken(); // Recurse
|
||||
|
||||
if (value2 != null)
|
||||
{
|
||||
jobject[key] = value2;
|
||||
}
|
||||
}
|
||||
|
||||
result = jobject;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Internal error reading the template. Expected a string, an array, or a dictionary");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static TemplateToken ToTemplateToken(this PipelineContextData data)
|
||||
{
|
||||
if (data is null)
|
||||
{
|
||||
return new NullToken(null, null, null);
|
||||
}
|
||||
|
||||
switch (data.Type)
|
||||
{
|
||||
case PipelineContextDataType.Dictionary:
|
||||
var dictionary = data.AssertDictionary("dictionary");
|
||||
var mapping = new MappingToken(null, null, null);
|
||||
if (dictionary.Count > 0)
|
||||
{
|
||||
foreach (var pair in dictionary)
|
||||
{
|
||||
var key = new StringToken(null, null, null, pair.Key);
|
||||
var value = pair.Value.ToTemplateToken();
|
||||
mapping.Add(key, value);
|
||||
}
|
||||
}
|
||||
return mapping;
|
||||
|
||||
case PipelineContextDataType.Array:
|
||||
var array = data.AssertArray("array");
|
||||
var sequence = new SequenceToken(null, null, null);
|
||||
if (array.Count > 0)
|
||||
{
|
||||
foreach (var item in array)
|
||||
{
|
||||
sequence.Add(item.ToTemplateToken());
|
||||
}
|
||||
}
|
||||
return sequence;
|
||||
|
||||
case PipelineContextDataType.String:
|
||||
var stringData = data as StringContextData;
|
||||
return new StringToken(null, null, null, stringData.Value);
|
||||
|
||||
case PipelineContextDataType.Boolean:
|
||||
var booleanData = data as BooleanContextData;
|
||||
return new BooleanToken(null, null, null, booleanData.Value);
|
||||
|
||||
case PipelineContextDataType.Number:
|
||||
var numberData = data as NumberContextData;
|
||||
return new NumberToken(null, null, null, numberData.Value);
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected {nameof(PipelineContextDataType)} type '{data.Type}'");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TraversalState
|
||||
{
|
||||
public TraversalState(
|
||||
TraversalState parent,
|
||||
PipelineContextData data)
|
||||
{
|
||||
Parent = parent;
|
||||
m_data = data;
|
||||
}
|
||||
|
||||
public Boolean MoveNext(Boolean omitKeys)
|
||||
{
|
||||
switch (m_data.Type)
|
||||
{
|
||||
case PipelineContextDataType.Array:
|
||||
var array = m_data.AssertArray("array");
|
||||
if (++m_index < array.Count)
|
||||
{
|
||||
Current = array[m_index];
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
case PipelineContextDataType.Dictionary:
|
||||
var dictionary = m_data.AssertDictionary("dictionary");
|
||||
|
||||
// Return the value
|
||||
if (m_isKey)
|
||||
{
|
||||
m_isKey = false;
|
||||
Current = dictionary[m_index].Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (++m_index < dictionary.Count)
|
||||
{
|
||||
// Skip the key, return the value
|
||||
if (omitKeys)
|
||||
{
|
||||
m_isKey = false;
|
||||
Current = dictionary[m_index].Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return the key
|
||||
m_isKey = true;
|
||||
Current = new StringContextData(dictionary[m_index].Key);
|
||||
return true;
|
||||
}
|
||||
|
||||
Current = null;
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected {nameof(PipelineContextData)} type '{m_data.Type}'");
|
||||
}
|
||||
}
|
||||
|
||||
private PipelineContextData m_data;
|
||||
private Int32 m_index = -1;
|
||||
private Boolean m_isKey;
|
||||
public PipelineContextData Current;
|
||||
public TraversalState Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON serializer for ContextData objects
|
||||
/// </summary>
|
||||
internal sealed class PipelineContextDataJsonConverter : VssSecureJsonConverter
|
||||
{
|
||||
public override Boolean CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public override Boolean CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(PipelineContextData).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override Object ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
Object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
return new StringContextData(reader.Value.ToString());
|
||||
|
||||
case JsonToken.Boolean:
|
||||
return new BooleanContextData((Boolean)reader.Value);
|
||||
|
||||
case JsonToken.Float:
|
||||
return new NumberContextData((Double)reader.Value);
|
||||
|
||||
case JsonToken.Integer:
|
||||
return new NumberContextData((Double)(Int64)reader.Value);
|
||||
|
||||
case JsonToken.StartObject:
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
Int32? type = null;
|
||||
JObject value = JObject.Load(reader);
|
||||
if (!value.TryGetValue("t", StringComparison.OrdinalIgnoreCase, out JToken typeValue))
|
||||
{
|
||||
type = PipelineContextDataType.String;
|
||||
}
|
||||
else if (typeValue.Type == JTokenType.Integer)
|
||||
{
|
||||
type = (Int32)typeValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return existingValue;
|
||||
}
|
||||
|
||||
Object newValue = null;
|
||||
switch (type)
|
||||
{
|
||||
case PipelineContextDataType.String:
|
||||
newValue = new StringContextData(null);
|
||||
break;
|
||||
|
||||
case PipelineContextDataType.Array:
|
||||
newValue = new ArrayContextData();
|
||||
break;
|
||||
|
||||
case PipelineContextDataType.Dictionary:
|
||||
newValue = new DictionaryContextData();
|
||||
break;
|
||||
|
||||
case PipelineContextDataType.Boolean:
|
||||
newValue = new BooleanContextData(false);
|
||||
break;
|
||||
|
||||
case PipelineContextDataType.Number:
|
||||
newValue = new NumberContextData(0);
|
||||
break;
|
||||
|
||||
case PipelineContextDataType.CaseSensitiveDictionary:
|
||||
newValue = new CaseSensitiveDictionaryContextData();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected {nameof(PipelineContextDataType)} '{type}'");
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
using (JsonReader objectReader = value.CreateReader())
|
||||
{
|
||||
serializer.Populate(objectReader, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public override void WriteJson(
|
||||
JsonWriter writer,
|
||||
Object value,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
base.WriteJson(writer, value, serializer);
|
||||
if (Object.ReferenceEquals(value, null))
|
||||
{
|
||||
writer.WriteNull();
|
||||
}
|
||||
else if (value is StringContextData stringData)
|
||||
{
|
||||
writer.WriteValue(stringData.Value);
|
||||
}
|
||||
else if (value is BooleanContextData boolData)
|
||||
{
|
||||
writer.WriteValue(boolData.Value);
|
||||
}
|
||||
else if (value is NumberContextData numberData)
|
||||
{
|
||||
writer.WriteValue(numberData.Value);
|
||||
}
|
||||
else if (value is ArrayContextData arrayData)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("t");
|
||||
writer.WriteValue(PipelineContextDataType.Array);
|
||||
if (arrayData.Count > 0)
|
||||
{
|
||||
writer.WritePropertyName("a");
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in arrayData)
|
||||
{
|
||||
serializer.Serialize(writer, item);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
else if (value is DictionaryContextData dictionaryData)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("t");
|
||||
writer.WriteValue(PipelineContextDataType.Dictionary);
|
||||
if (dictionaryData.Count > 0)
|
||||
{
|
||||
writer.WritePropertyName("d");
|
||||
writer.WriteStartArray();
|
||||
foreach (var pair in dictionaryData)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("k");
|
||||
writer.WriteValue(pair.Key);
|
||||
writer.WritePropertyName("v");
|
||||
serializer.Serialize(writer, pair.Value);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
else if (value is CaseSensitiveDictionaryContextData caseSensitiveDictionaryData)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("t");
|
||||
writer.WriteValue(PipelineContextDataType.CaseSensitiveDictionary);
|
||||
if (caseSensitiveDictionaryData.Count > 0)
|
||||
{
|
||||
writer.WritePropertyName("d");
|
||||
writer.WriteStartArray();
|
||||
foreach (var pair in caseSensitiveDictionaryData)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("k");
|
||||
writer.WriteValue(pair.Key);
|
||||
writer.WritePropertyName("v");
|
||||
serializer.Serialize(writer, pair.Value);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Unexpected type '{value.GetType().Name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
internal static class PipelineContextDataType
|
||||
{
|
||||
internal const Int32 String = 0;
|
||||
|
||||
internal const Int32 Array = 1;
|
||||
|
||||
internal const Int32 Dictionary = 2;
|
||||
|
||||
internal const Int32 Boolean = 3;
|
||||
|
||||
internal const Int32 Number = 4;
|
||||
|
||||
internal const Int32 CaseSensitiveDictionary = 5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.Services.WebApi.Internal;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[DataContract]
|
||||
[ClientIgnore]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class StringContextData : PipelineContextData, IString
|
||||
{
|
||||
public StringContextData(String value)
|
||||
: base(PipelineContextDataType.String)
|
||||
{
|
||||
m_value = value;
|
||||
}
|
||||
|
||||
public String Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_value == null)
|
||||
{
|
||||
m_value = String.Empty;
|
||||
}
|
||||
|
||||
return m_value;
|
||||
}
|
||||
}
|
||||
|
||||
public override PipelineContextData Clone()
|
||||
{
|
||||
return new StringContextData(m_value);
|
||||
}
|
||||
|
||||
public override JToken ToJToken()
|
||||
{
|
||||
return (JToken)m_value;
|
||||
}
|
||||
|
||||
String IString.GetString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
public override String ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
public static implicit operator String(StringContextData data)
|
||||
{
|
||||
return data.Value;
|
||||
}
|
||||
|
||||
public static implicit operator StringContextData(String data)
|
||||
{
|
||||
return new StringContextData(data);
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_value?.Length == 0)
|
||||
{
|
||||
m_value = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "s", EmitDefaultValue = false)]
|
||||
private String m_value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.ObjectTemplating;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
internal static class TemplateMemoryExtensions
|
||||
{
|
||||
internal static void AddBytes(
|
||||
this TemplateMemory memory,
|
||||
PipelineContextData value,
|
||||
Boolean traverse)
|
||||
{
|
||||
var bytes = CalculateBytes(memory, value, traverse);
|
||||
memory.AddBytes(bytes);
|
||||
}
|
||||
|
||||
internal static Int32 CalculateBytes(
|
||||
this TemplateMemory memory,
|
||||
PipelineContextData value,
|
||||
Boolean traverse)
|
||||
{
|
||||
var enumerable = traverse ? value.Traverse() : new[] { value } as IEnumerable<PipelineContextData>;
|
||||
var result = 0;
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
// This measurement doesn't have to be perfect
|
||||
// https://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/
|
||||
switch (item?.Type)
|
||||
{
|
||||
case PipelineContextDataType.String:
|
||||
var str = item.AssertString("string").Value;
|
||||
checked
|
||||
{
|
||||
result += TemplateMemory.MinObjectSize + TemplateMemory.StringBaseOverhead + ((str?.Length ?? 0) * sizeof(Char));
|
||||
}
|
||||
break;
|
||||
|
||||
case PipelineContextDataType.Array:
|
||||
case PipelineContextDataType.Dictionary:
|
||||
case PipelineContextDataType.Boolean:
|
||||
case PipelineContextDataType.Number:
|
||||
// Min object size is good enough. Allows for base + a few fields.
|
||||
checked
|
||||
{
|
||||
result += TemplateMemory.MinObjectSize;
|
||||
}
|
||||
break;
|
||||
|
||||
case null:
|
||||
checked
|
||||
{
|
||||
result += IntPtr.Size;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected pipeline context data type '{item.Type}'");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.ContextData
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class TemplateTokenExtensions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static StringContextData ToContextData(this LiteralToken literal)
|
||||
{
|
||||
var token = literal as TemplateToken;
|
||||
var contextData = token.ToContextData();
|
||||
return contextData.AssertString("converted literal token");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static ArrayContextData ToContextData(this SequenceToken sequence)
|
||||
{
|
||||
var token = sequence as TemplateToken;
|
||||
var contextData = token.ToContextData();
|
||||
return contextData.AssertArray("converted sequence token");
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static PipelineContextData ToContextData(this TemplateToken token)
|
||||
{
|
||||
switch (token.Type)
|
||||
{
|
||||
case TokenType.Mapping:
|
||||
var mapping = token as MappingToken;
|
||||
var dictionary = new DictionaryContextData();
|
||||
if (mapping.Count > 0)
|
||||
{
|
||||
foreach (var pair in mapping)
|
||||
{
|
||||
var keyLiteral = pair.Key.AssertString("dictionary context data key");
|
||||
var key = keyLiteral.Value;
|
||||
var value = pair.Value.ToContextData();
|
||||
dictionary.Add(key, value);
|
||||
}
|
||||
}
|
||||
return dictionary;
|
||||
|
||||
case TokenType.Sequence:
|
||||
var sequence = token as SequenceToken;
|
||||
var array = new ArrayContextData();
|
||||
if (sequence.Count > 0)
|
||||
{
|
||||
foreach (var item in sequence)
|
||||
{
|
||||
array.Add(item.ToContextData());
|
||||
}
|
||||
}
|
||||
return array;
|
||||
|
||||
case TokenType.Null:
|
||||
return null;
|
||||
|
||||
case TokenType.Boolean:
|
||||
var boolean = token as BooleanToken;
|
||||
return new BooleanContextData(boolean.Value);
|
||||
|
||||
case TokenType.Number:
|
||||
var number = token as NumberToken;
|
||||
return new NumberContextData(number.Value);
|
||||
|
||||
case TokenType.String:
|
||||
var stringToken = token as StringToken;
|
||||
return new StringContextData(stringToken.Value);
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unexpected {nameof(TemplateToken)} type '{token.Type}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/Sdk/DTPipelines/Pipelines/ContextScope.cs
Normal file
53
src/Sdk/DTPipelines/Pipelines/ContextScope.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class ContextScope
|
||||
{
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String Name { get; set; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public String ContextName
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = Name.LastIndexOf('.');
|
||||
if (index >= 0)
|
||||
{
|
||||
return Name.Substring(index + 1);
|
||||
}
|
||||
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public String ParentName
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = Name.LastIndexOf('.');
|
||||
if (index >= 0)
|
||||
{
|
||||
return Name.Substring(0, index);
|
||||
}
|
||||
|
||||
return String.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken Inputs { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken Outputs { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class ContinuousIntegrationTrigger : PipelineTrigger
|
||||
{
|
||||
public ContinuousIntegrationTrigger()
|
||||
: base(PipelineTriggerType.ContinuousIntegration)
|
||||
{
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = true)]
|
||||
public Boolean Enabled
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether changes should be batched while another CI pipeline is running.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this is true, then changes submitted while a CI pipeline is running will be batched and built in one new CI pipeline when the current pipeline finishes.
|
||||
/// If this is false, then a new CI pipeline will be triggered for each change to the repository.
|
||||
/// </remarks>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Boolean BatchChanges
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of filters that describe which branches will trigger pipelines.
|
||||
/// </summary>
|
||||
public IList<String> BranchFilters
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_branchFilters == null)
|
||||
{
|
||||
m_branchFilters = new List<String>();
|
||||
}
|
||||
return m_branchFilters;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of filters that describe which paths will trigger pipelines.
|
||||
/// </summary>
|
||||
public IList<String> PathFilters
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_pathFilters == null)
|
||||
{
|
||||
m_pathFilters = new List<String>();
|
||||
}
|
||||
return m_pathFilters;
|
||||
}
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_branchFilters?.Count == 0)
|
||||
{
|
||||
m_branchFilters = null;
|
||||
}
|
||||
|
||||
if (m_pathFilters?.Count == 0)
|
||||
{
|
||||
m_pathFilters = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "BranchFilters", EmitDefaultValue = false)]
|
||||
private List<String> m_branchFilters;
|
||||
|
||||
[DataMember(Name = "PathFilters", EmitDefaultValue = false)]
|
||||
private List<String> m_pathFilters;
|
||||
}
|
||||
}
|
||||
61
src/Sdk/DTPipelines/Pipelines/CounterStore.cs
Normal file
61
src/Sdk/DTPipelines/Pipelines/CounterStore.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.Services.Common;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a default implementation of a counter store.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class CounterStore : ICounterStore
|
||||
{
|
||||
public CounterStore(
|
||||
IDictionary<String, Int32> counters = null,
|
||||
ICounterResolver resolver = null)
|
||||
{
|
||||
if (counters?.Count > 0)
|
||||
{
|
||||
m_counters.AddRange(counters);
|
||||
}
|
||||
|
||||
this.Resolver = resolver;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<String, Int32> Counters
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_counters;
|
||||
}
|
||||
}
|
||||
|
||||
private ICounterResolver Resolver
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public Int32 Increment(
|
||||
IPipelineContext context,
|
||||
String prefix,
|
||||
Int32 seed)
|
||||
{
|
||||
if (m_counters.TryGetValue(prefix, out Int32 existingValue))
|
||||
{
|
||||
return existingValue;
|
||||
}
|
||||
|
||||
Int32 newValue = seed;
|
||||
if (this.Resolver != null)
|
||||
{
|
||||
newValue = this.Resolver.Increment(context, prefix, seed);
|
||||
m_counters[prefix] = newValue;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
private readonly Dictionary<String, Int32> m_counters = new Dictionary<String, Int32>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
50
src/Sdk/DTPipelines/Pipelines/CreateJobResult.cs
Normal file
50
src/Sdk/DTPipelines/Pipelines/CreateJobResult.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public struct CreateJobResult
|
||||
{
|
||||
public CreateJobResult(
|
||||
JobExecutionContext context,
|
||||
Job job)
|
||||
{
|
||||
this.Job = job;
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
public Job Job
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public JobExecutionContext Context
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public struct CreateTaskResult
|
||||
{
|
||||
public CreateTaskResult(
|
||||
TaskStep task,
|
||||
TaskDefinition definition)
|
||||
{
|
||||
this.Task = task;
|
||||
this.Definition = definition;
|
||||
}
|
||||
|
||||
public TaskStep Task
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public TaskDefinition Definition
|
||||
{
|
||||
get;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/Sdk/DTPipelines/Pipelines/DeploymentExecutionOptions.cs
Normal file
74
src/Sdk/DTPipelines/Pipelines/DeploymentExecutionOptions.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Pipelines.Validation;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
internal enum DeploymentRollingOption
|
||||
{
|
||||
[EnumMember]
|
||||
Absolute,
|
||||
|
||||
[EnumMember]
|
||||
Percentage
|
||||
}
|
||||
|
||||
[DataContract]
|
||||
internal class DeploymentExecutionOptions
|
||||
{
|
||||
public DeploymentExecutionOptions()
|
||||
{
|
||||
}
|
||||
|
||||
private DeploymentExecutionOptions(DeploymentExecutionOptions optionsToCopy)
|
||||
{
|
||||
this.RollingOption = optionsToCopy.RollingOption;
|
||||
this.RollingValue = optionsToCopy.RollingValue;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public DeploymentRollingOption RollingOption
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public uint RollingValue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public DeploymentExecutionOptions Clone()
|
||||
{
|
||||
return new DeploymentExecutionOptions(this);
|
||||
}
|
||||
|
||||
public void Validate(
|
||||
IPipelineContext context,
|
||||
ValidationResult result)
|
||||
{
|
||||
switch (RollingOption)
|
||||
{
|
||||
case DeploymentRollingOption.Absolute:
|
||||
if (RollingValue == 0)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.InvalidAbsoluteRollingValue()));
|
||||
}
|
||||
break;
|
||||
|
||||
case DeploymentRollingOption.Percentage:
|
||||
if (RollingValue == 0 || RollingValue > 100)
|
||||
{
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.InvalidPercentageRollingValue()));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
result.Errors.Add(new PipelineValidationError(PipelineStrings.InvalidRollingOption(RollingOption)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/Sdk/DTPipelines/Pipelines/DeploymentGroupTarget.cs
Normal file
158
src/Sdk/DTPipelines/Pipelines/DeploymentGroupTarget.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
using GitHub.DistributedTask.Pipelines.Validation;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
internal class DeploymentGroupTarget : PhaseTarget
|
||||
{
|
||||
public DeploymentGroupTarget()
|
||||
: base(PhaseTargetType.DeploymentGroup)
|
||||
{
|
||||
}
|
||||
|
||||
private DeploymentGroupTarget(DeploymentGroupTarget targetToClone)
|
||||
: base(targetToClone)
|
||||
{
|
||||
this.DeploymentGroup = targetToClone.DeploymentGroup?.Clone();
|
||||
this.Execution = targetToClone.Execution?.Clone();
|
||||
|
||||
if (targetToClone.m_tags != null && targetToClone.m_tags.Count > 0)
|
||||
{
|
||||
m_tags = new HashSet<String>(targetToClone.m_tags, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public DeploymentGroupReference DeploymentGroup
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public ISet<String> Tags
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_tags == null)
|
||||
{
|
||||
m_tags = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
return m_tags;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets targets Ids filter on which deployment should be done.
|
||||
/// </summary>
|
||||
public List<Int32> TargetIds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_targetIds == null)
|
||||
{
|
||||
m_targetIds = new List<Int32>();
|
||||
}
|
||||
return m_targetIds;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public DeploymentExecutionOptions Execution
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override PhaseTarget Clone()
|
||||
{
|
||||
return new DeploymentGroupTarget(this);
|
||||
}
|
||||
|
||||
public override Boolean IsValid(TaskDefinition task)
|
||||
{
|
||||
return task.RunsOn.Contains(TaskRunsOnConstants.RunsOnDeploymentGroup, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal override void Validate(
|
||||
IPipelineContext context,
|
||||
BuildOptions buildOptions,
|
||||
ValidationResult result,
|
||||
IList<Step> steps,
|
||||
ISet<Demand> taskDemands)
|
||||
{
|
||||
this.Execution?.Validate(context, result);
|
||||
}
|
||||
|
||||
internal override JobExecutionContext CreateJobContext(
|
||||
PhaseExecutionContext context,
|
||||
String jobName,
|
||||
Int32 attempt,
|
||||
Boolean continueOnError,
|
||||
Int32 timeoutInMinutes,
|
||||
Int32 cancelTimeoutInMinutes,
|
||||
IJobFactory jobFactory)
|
||||
{
|
||||
context.Trace?.EnterProperty("CreateJobContext");
|
||||
var result = new ParallelExecutionOptions().CreateJobContext(
|
||||
context,
|
||||
jobName,
|
||||
attempt,
|
||||
null,
|
||||
null,
|
||||
continueOnError,
|
||||
timeoutInMinutes,
|
||||
cancelTimeoutInMinutes,
|
||||
jobFactory);
|
||||
context.Trace?.LeaveProperty("CreateJobContext");
|
||||
return result;
|
||||
}
|
||||
|
||||
internal override ExpandPhaseResult Expand(
|
||||
PhaseExecutionContext context,
|
||||
Boolean continueOnError,
|
||||
Int32 timeoutInMinutes,
|
||||
Int32 cancelTimeoutInMinutes,
|
||||
IJobFactory jobFactory,
|
||||
JobExpansionOptions options)
|
||||
{
|
||||
context.Trace?.EnterProperty("Expand");
|
||||
var result = new ParallelExecutionOptions().Expand(
|
||||
context: context,
|
||||
container: null,
|
||||
sidecarContainers: null,
|
||||
continueOnError: continueOnError,
|
||||
timeoutInMinutes: timeoutInMinutes,
|
||||
cancelTimeoutInMinutes: cancelTimeoutInMinutes,
|
||||
jobFactory: jobFactory,
|
||||
options: options);
|
||||
context.Trace?.LeaveProperty("Expand");
|
||||
return result;
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_tags?.Count == 0)
|
||||
{
|
||||
m_tags = null;
|
||||
}
|
||||
|
||||
if (m_targetIds?.Count == 0)
|
||||
{
|
||||
m_targetIds = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "Tags", EmitDefaultValue = false)]
|
||||
private ISet<String> m_tags;
|
||||
|
||||
[DataMember(Name = "TargetIds")]
|
||||
private List<Int32> m_targetIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class EnvironmentReference : ResourceReference
|
||||
{
|
||||
public EnvironmentReference()
|
||||
{
|
||||
}
|
||||
|
||||
private EnvironmentReference(EnvironmentReference referenceToCopy)
|
||||
: base(referenceToCopy)
|
||||
{
|
||||
this.Id = referenceToCopy.Id;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Int32 Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public EnvironmentReference Clone()
|
||||
{
|
||||
return new EnvironmentReference(this);
|
||||
}
|
||||
|
||||
public override String ToString()
|
||||
{
|
||||
return base.ToString() ?? this.Id.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Sdk/DTPipelines/Pipelines/EnvironmentDeploymentTarget.cs
Normal file
21
src/Sdk/DTPipelines/Pipelines/EnvironmentDeploymentTarget.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class EnvironmentDeploymentTarget
|
||||
{
|
||||
[DataMember]
|
||||
public Int32 EnvironmentId { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public String EnvironmentName { get; set; }
|
||||
|
||||
[DataMember]
|
||||
public EnvironmentResourceReference Resource { get; set; }
|
||||
}
|
||||
}
|
||||
97
src/Sdk/DTPipelines/Pipelines/EnvironmentStore.cs
Normal file
97
src/Sdk/DTPipelines/Pipelines/EnvironmentStore.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class EnvironmentStore : IEnvironmentStore
|
||||
{
|
||||
public EnvironmentStore(
|
||||
IList<EnvironmentInstance> environments,
|
||||
IEnvironmentResolver resolver = null)
|
||||
{
|
||||
m_resolver = resolver;
|
||||
m_environmentsByName = new Dictionary<String, EnvironmentInstance>(StringComparer.OrdinalIgnoreCase);
|
||||
m_environmentsById = new Dictionary<Int32, EnvironmentInstance>();
|
||||
Add(environments?.ToArray());
|
||||
}
|
||||
|
||||
public void Add(params EnvironmentInstance[] environments)
|
||||
{
|
||||
if (environments is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var e in environments)
|
||||
{
|
||||
if (e != null)
|
||||
{
|
||||
m_environmentsById[e.Id] = e;
|
||||
|
||||
var name = e.Name;
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
m_environmentsByName[name] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public EnvironmentInstance ResolveEnvironment(String name)
|
||||
{
|
||||
if (!m_environmentsByName.TryGetValue(name, out var environment)
|
||||
&& m_resolver != null)
|
||||
{
|
||||
environment = m_resolver?.Resolve(name);
|
||||
Add(environment);
|
||||
}
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
public EnvironmentInstance ResolveEnvironment(Int32 id)
|
||||
{
|
||||
if (!m_environmentsById.TryGetValue(id, out var environment)
|
||||
&& m_resolver != null)
|
||||
{
|
||||
environment = m_resolver?.Resolve(id);
|
||||
Add(environment);
|
||||
}
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
public EnvironmentInstance Get(EnvironmentReference reference)
|
||||
{
|
||||
if (reference is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (reference.Name?.IsLiteral == true)
|
||||
{
|
||||
return ResolveEnvironment(reference.Name.Literal);
|
||||
}
|
||||
|
||||
return ResolveEnvironment(reference.Id);
|
||||
}
|
||||
|
||||
public IList<EnvironmentReference> GetReferences()
|
||||
{
|
||||
return m_environmentsById.Values
|
||||
.Select(x => new EnvironmentReference
|
||||
{
|
||||
Id = x.Id,
|
||||
Name = x.Name
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnvironmentResolver m_resolver;
|
||||
private IDictionary<String, EnvironmentInstance> m_environmentsByName;
|
||||
private IDictionary<Int32, EnvironmentInstance> m_environmentsById;
|
||||
}
|
||||
}
|
||||
107
src/Sdk/DTPipelines/Pipelines/ExecutionOptions.cs
Normal file
107
src/Sdk/DTPipelines/Pipelines/ExecutionOptions.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism for controlling runtime behaviors.
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class ExecutionOptions
|
||||
{
|
||||
public ExecutionOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not to remove secrets from job message.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Boolean RestrictSecrets
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating what scope the system jwt token will have.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String SystemTokenScope
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets value indicating any custom claims the system jwt token will have.
|
||||
/// </summary>
|
||||
public IDictionary<String, String> SystemTokenCustomClaims
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_systemTokenCustomClaims == null)
|
||||
{
|
||||
m_systemTokenCustomClaims = new Dictionary<String, String>();
|
||||
}
|
||||
return m_systemTokenCustomClaims;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating what's the max number jobs we allow after expansion.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Int32? MaxJobExpansion
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the max parallelism slots available to overwrite MaxConcurrency of test job slicing
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Int32? MaxParallelism
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating if we should allow expressions to define secured resources.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Boolean EnableResourceExpressions
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Driven by FF: DistributedTask.LegalNodeNames
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Boolean EnforceLegalNodeNames
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows hyphens in yaml names
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Boolean AllowHyphenNames
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(Name = nameof(SystemTokenCustomClaims), EmitDefaultValue = false)]
|
||||
private IDictionary<String, String> m_systemTokenCustomClaims;
|
||||
}
|
||||
}
|
||||
66
src/Sdk/DTPipelines/Pipelines/ExpandPhaseResult.cs
Normal file
66
src/Sdk/DTPipelines/Pipelines/ExpandPhaseResult.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the runtime values of a phase which has been expanded for execution.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class ExpandPhaseResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new <c>ExpandPhaseResult</c> innstance with a default maximum concurrency of 1.
|
||||
/// </summary>
|
||||
public ExpandPhaseResult()
|
||||
{
|
||||
this.MaxConcurrency = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution behavior when an error is encountered.
|
||||
/// </summary>
|
||||
public Boolean ContinueOnError
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution behavior when an error is encountered.
|
||||
/// </summary>
|
||||
public Boolean FailFast
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum concurrency for the jobs.
|
||||
/// </summary>
|
||||
public Int32 MaxConcurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of jobs for this phase.
|
||||
/// </summary>
|
||||
public IList<JobInstance> Jobs
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_jobs == null)
|
||||
{
|
||||
m_jobs = new List<JobInstance>();
|
||||
}
|
||||
return m_jobs;
|
||||
}
|
||||
}
|
||||
|
||||
private List<JobInstance> m_jobs;
|
||||
}
|
||||
}
|
||||
58
src/Sdk/DTPipelines/Pipelines/ExpressionResult.cs
Normal file
58
src/Sdk/DTPipelines/Pipelines/ExpressionResult.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the result of an <c>ExpressionValue<T></c> evaluation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class ExpressionResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new <c>ExpressionResult</c> instance with the specified value. The value is implicilty treated as
|
||||
/// non-secret.
|
||||
/// </summary>
|
||||
/// <param name="value">The resolved value</param>
|
||||
public ExpressionResult(T value)
|
||||
: this(value, false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>ExpressionResult</c> instance with the specified values.
|
||||
/// </summary>
|
||||
/// <param name="value">The resolved value</param>
|
||||
/// <param name="containsSecrets">True if secrets were accessed while resolving the value; otherwise, false</param>
|
||||
public ExpressionResult(
|
||||
T value,
|
||||
Boolean containsSecrets)
|
||||
{
|
||||
this.ContainsSecrets = containsSecrets;
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not secrets were accessed while resolving <see cref="Value"/>.
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Boolean ContainsSecrets
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the literal value result.
|
||||
/// </summary>
|
||||
[DataMember]
|
||||
public T Value
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
311
src/Sdk/DTPipelines/Pipelines/ExpressionValue.cs
Normal file
311
src/Sdk/DTPipelines/Pipelines/ExpressionValue.cs
Normal file
@@ -0,0 +1,311 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism for performing delayed evaluation of a value based on the environment context as runtime.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public abstract class ExpressionValue
|
||||
{
|
||||
public static Boolean IsExpression(String value)
|
||||
{
|
||||
return !String.IsNullOrEmpty(value) &&
|
||||
value.Length > 3 &&
|
||||
value.StartsWith("$[", StringComparison.Ordinal) &&
|
||||
value.EndsWith("]", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the specified string as an expression value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The expected type of the expression result</typeparam>
|
||||
/// <param name="expression">The expression string</param>
|
||||
/// <param name="value">The value which was parsed, if any</param>
|
||||
/// <returns>True if the value was successfully parsed; otherwise, false</returns>
|
||||
public static Boolean TryParse<T>(
|
||||
String expression,
|
||||
out ExpressionValue<T> value)
|
||||
{
|
||||
if (IsExpression(expression))
|
||||
{
|
||||
value = new ExpressionValue<T>(expression, isExpression: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = null;
|
||||
}
|
||||
return value != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ExpressionValue from expression string.
|
||||
/// Returns null if argument is not an expression
|
||||
/// </summary>
|
||||
public static ExpressionValue<T> FromExpression<T>(String expression)
|
||||
{
|
||||
return new ExpressionValue<T>(expression, isExpression: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ExpressionValue from literal.
|
||||
/// </summary>
|
||||
public static ExpressionValue<T> FromLiteral<T>(T literal)
|
||||
{
|
||||
return new ExpressionValue<T>(literal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When T is String, we cannot distiguish between literals and expressions solely by type.
|
||||
/// Use this function when parsing and you want to err on the side of expressions.
|
||||
/// </summary>
|
||||
public static ExpressionValue<String> FromToken(String token)
|
||||
{
|
||||
if (ExpressionValue.IsExpression(token))
|
||||
{
|
||||
return ExpressionValue.FromExpression<String>(token);
|
||||
}
|
||||
|
||||
return ExpressionValue.FromLiteral(token);
|
||||
}
|
||||
|
||||
internal static String TrimExpression(String value)
|
||||
{
|
||||
var expression = value.Substring(2, value.Length - 3).Trim();
|
||||
if (String.IsNullOrEmpty(expression))
|
||||
{
|
||||
throw new ArgumentException(PipelineStrings.ExpressionInvalid(value));
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a mechanism for performing delayed evaluation of a value based on the environment context at runtime.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value</typeparam>
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class ExpressionValue<T> : ExpressionValue, IEquatable<ExpressionValue<T>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new <c>ExpressionValue</c> instance with the specified literal value.
|
||||
/// </summary>
|
||||
/// <param name="literalValue">The literal value which should be used</param>
|
||||
public ExpressionValue(T literalValue)
|
||||
{
|
||||
m_literalValue = literalValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>ExpressionValue</c> with the given expression.
|
||||
/// Throws if expression is invalid.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to be used</param>
|
||||
/// <param name="isExpression">This parameter is unused other than to discriminate this constructor from the literal constructor</param>
|
||||
internal ExpressionValue(
|
||||
String expression,
|
||||
Boolean isExpression)
|
||||
{
|
||||
if (!IsExpression(expression))
|
||||
{
|
||||
throw new ArgumentException(PipelineStrings.ExpressionInvalid(expression));
|
||||
}
|
||||
m_expression = ExpressionValue.TrimExpression(expression);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
private ExpressionValue()
|
||||
{
|
||||
}
|
||||
|
||||
internal T Literal
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_literalValue;
|
||||
}
|
||||
}
|
||||
|
||||
internal String Expression
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_expression;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the expression is backed by a literal value.
|
||||
/// </summary>
|
||||
internal Boolean IsLiteral => String.IsNullOrEmpty(m_expression);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the referenced value from the provided execution context.
|
||||
/// </summary>
|
||||
/// <param name="context">The execution context used for variable resolution</param>
|
||||
/// <returns>The value of the variable if found; otherwise, null</returns>
|
||||
public ExpressionResult<T> GetValue(IPipelineContext context = null)
|
||||
{
|
||||
if (this.IsLiteral)
|
||||
{
|
||||
return new ExpressionResult<T>(m_literalValue, containsSecrets: false);
|
||||
}
|
||||
|
||||
if (context != null)
|
||||
{
|
||||
return context.Evaluate<T>(m_expression);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the value to a string representation.
|
||||
/// </summary>
|
||||
/// <returns>A string representation of the current value</returns>
|
||||
public override String ToString()
|
||||
{
|
||||
if (!String.IsNullOrEmpty(m_expression))
|
||||
{
|
||||
return String.Concat("$[ ", m_expression, " ]");
|
||||
}
|
||||
else
|
||||
{
|
||||
return m_literalValue?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides automatic conversion of a literal value into a pipeline value for convenience.
|
||||
/// </summary>
|
||||
/// <param name="value">The value which the pipeline value represents</param>
|
||||
public static implicit operator ExpressionValue<T>(T value)
|
||||
{
|
||||
return new ExpressionValue<T>(value);
|
||||
}
|
||||
|
||||
public Boolean Equals(ExpressionValue<T> rhs)
|
||||
{
|
||||
if (rhs is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, rhs))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsLiteral)
|
||||
{
|
||||
return EqualityComparer<T>.Default.Equals(this.Literal, rhs.Literal);
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.Expression == rhs.Expression;
|
||||
}
|
||||
}
|
||||
|
||||
public override Boolean Equals(object obj)
|
||||
{
|
||||
return Equals(obj as ExpressionValue<T>);
|
||||
}
|
||||
|
||||
public static Boolean operator ==(ExpressionValue<T> lhs, ExpressionValue<T> rhs)
|
||||
{
|
||||
if (lhs is null)
|
||||
{
|
||||
return rhs is null;
|
||||
}
|
||||
|
||||
return lhs.Equals(rhs);
|
||||
}
|
||||
public static Boolean operator !=(ExpressionValue<T> lhs, ExpressionValue<T> rhs)
|
||||
{
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
public override Int32 GetHashCode()
|
||||
{
|
||||
if (IsLiteral)
|
||||
{
|
||||
if (Literal != null)
|
||||
{
|
||||
return Literal.GetHashCode();
|
||||
}
|
||||
}
|
||||
else if (Expression != null)
|
||||
{
|
||||
return Expression.GetHashCode();
|
||||
}
|
||||
|
||||
return 0; // unspecified expression values are all the same.
|
||||
}
|
||||
|
||||
[DataMember(Name = "LiteralValue", EmitDefaultValue = false)]
|
||||
private readonly T m_literalValue;
|
||||
|
||||
[DataMember(Name = "VariableValue", EmitDefaultValue = false)]
|
||||
private readonly String m_expression;
|
||||
}
|
||||
|
||||
internal class ExpressionValueJsonConverter<T> : VssSecureJsonConverter
|
||||
{
|
||||
public override Boolean CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.GetTypeInfo().Equals(typeof(String).GetTypeInfo()) || typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override Object ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
Object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
// string types are either expressions of any type T, or literals of type String
|
||||
var s = (String)(Object)reader.Value;
|
||||
if (ExpressionValue.IsExpression(s))
|
||||
{
|
||||
return ExpressionValue.FromExpression<T>(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ExpressionValue<String>(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var parsedValue = serializer.Deserialize<T>(reader);
|
||||
return new ExpressionValue<T>(parsedValue);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(
|
||||
JsonWriter writer,
|
||||
Object value,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
base.WriteJson(writer, value, serializer);
|
||||
if (value is ExpressionValue<T> expressionValue)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(expressionValue.Expression))
|
||||
{
|
||||
serializer.Serialize(writer, $"$[ {expressionValue.Expression} ]");
|
||||
}
|
||||
else
|
||||
{
|
||||
serializer.Serialize(writer, expressionValue.Literal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Sdk/DTPipelines/Pipelines/Expressions/CounterNode.cs
Normal file
28
src/Sdk/DTPipelines/Pipelines/Expressions/CounterNode.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class CounterNode : FunctionNode
|
||||
{
|
||||
protected override Object EvaluateCore(EvaluationContext evaluationContext)
|
||||
{
|
||||
int seed = 0;
|
||||
var prefix = String.Empty;
|
||||
if (Parameters.Count > 0)
|
||||
{
|
||||
prefix = Parameters[0].EvaluateString(evaluationContext);
|
||||
}
|
||||
|
||||
if (Parameters.Count > 1)
|
||||
{
|
||||
seed = Convert.ToInt32(Parameters[1].EvaluateNumber(evaluationContext));
|
||||
}
|
||||
|
||||
var context = evaluationContext.State as IPipelineContext;
|
||||
return context.CounterStore?.Increment(context, prefix, seed) ?? seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class ExpressionConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the variables node.
|
||||
/// </summary>
|
||||
public static readonly String Variables = "variables";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pipeline context available in pipeline expressions.
|
||||
/// </summary>
|
||||
public static readonly INamedValueInfo PipelineNamedValue = new NamedValueInfo<PipelineContextNode>("pipeline");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the variable context available in pipeline expressions.
|
||||
/// </summary>
|
||||
public static readonly INamedValueInfo VariablesNamedValue = new NamedValueInfo<VariablesContextNode>("variables");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the counter function available in pipeline expressions.
|
||||
/// </summary>
|
||||
public static readonly IFunctionInfo CounterFunction = new FunctionInfo<CounterNode>("counter", 0, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
internal static class InputValidationConstants
|
||||
{
|
||||
public static readonly String IsEmail = "isEmail";
|
||||
public static readonly String IsInRange = "isInRange";
|
||||
public static readonly String IsIPv4Address = "isIPv4Address";
|
||||
public static readonly String IsSha1 = "isSha1";
|
||||
public static readonly String IsUrl = "isUrl";
|
||||
public static readonly String IsMatch = "isMatch";
|
||||
public static readonly String Length = "length";
|
||||
|
||||
public static readonly IFunctionInfo[] Functions = new IFunctionInfo[]
|
||||
{
|
||||
new FunctionInfo<IsEmailNode>(InputValidationConstants.IsEmail, IsEmailNode.minParameters, IsEmailNode.maxParameters),
|
||||
new FunctionInfo<IsInRangeNode>(InputValidationConstants.IsInRange, IsInRangeNode.minParameters, IsInRangeNode.maxParameters),
|
||||
new FunctionInfo<IsIPv4AddressNode>(InputValidationConstants.IsIPv4Address, IsIPv4AddressNode.minParameters, IsIPv4AddressNode.maxParameters),
|
||||
new FunctionInfo<IsMatchNode>(InputValidationConstants.IsMatch, IsMatchNode.minParameters, IsMatchNode.maxParameters),
|
||||
new FunctionInfo<IsSHA1Node>(InputValidationConstants.IsSha1, IsSHA1Node.minParameters, IsSHA1Node.maxParameters),
|
||||
new FunctionInfo<IsUrlNode>(InputValidationConstants.IsUrl, IsUrlNode.minParameters, IsUrlNode.maxParameters),
|
||||
new FunctionInfo<LengthNode>(InputValidationConstants.Length, LengthNode.minParameters, LengthNode.maxParameters),
|
||||
};
|
||||
|
||||
public static readonly INamedValueInfo[] NamedValues = new INamedValueInfo[]
|
||||
{
|
||||
new NamedValueInfo<InputValueNode>("value"),
|
||||
};
|
||||
}
|
||||
}
|
||||
15
src/Sdk/DTPipelines/Pipelines/Expressions/InputValueNode.cs
Normal file
15
src/Sdk/DTPipelines/Pipelines/Expressions/InputValueNode.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
using GitHub.DistributedTask.Pipelines.Validation;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
internal class InputValueNode : NamedValueNode
|
||||
{
|
||||
protected sealed override Object EvaluateCore(EvaluationContext evaluationContext)
|
||||
{
|
||||
var validationContext = evaluationContext.State as InputValidationContext;
|
||||
return validationContext.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Sdk/DTPipelines/Pipelines/Expressions/IsEmailNode.cs
Normal file
22
src/Sdk/DTPipelines/Pipelines/Expressions/IsEmailNode.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class IsEmailNode : FunctionNode
|
||||
{
|
||||
protected sealed override Boolean TraceFullyRealized => false;
|
||||
|
||||
public static Int32 minParameters = 1;
|
||||
public static Int32 maxParameters = 1;
|
||||
|
||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
// isEmail(value: string)
|
||||
String value = Parameters[0].EvaluateString(context) ?? String.Empty;
|
||||
return RegexUtility.IsMatch(value, WellKnownRegularExpressions.Email);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class IsIPv4AddressNode : FunctionNode
|
||||
{
|
||||
protected sealed override Boolean TraceFullyRealized => false;
|
||||
|
||||
public static Int32 minParameters = 1;
|
||||
public static Int32 maxParameters = 1;
|
||||
|
||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
// isIpV4Address(value: string)
|
||||
String value = Parameters[0].EvaluateString(context) ?? String.Empty;
|
||||
return RegexUtility.IsMatch(value, WellKnownRegularExpressions.IPv4Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Sdk/DTPipelines/Pipelines/Expressions/IsInRangeNode.cs
Normal file
24
src/Sdk/DTPipelines/Pipelines/Expressions/IsInRangeNode.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class IsInRangeNode : FunctionNode
|
||||
{
|
||||
protected sealed override Boolean TraceFullyRealized => false;
|
||||
|
||||
public static Int32 minParameters = 3;
|
||||
public static Int32 maxParameters = 3;
|
||||
|
||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
// isInRange(value: string, min: string, max: string)
|
||||
decimal value = Parameters[0].EvaluateNumber(context);
|
||||
decimal min = Parameters[1].EvaluateNumber(context);
|
||||
decimal max = Parameters[2].EvaluateNumber(context);
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Sdk/DTPipelines/Pipelines/Expressions/IsMatchNode.cs
Normal file
30
src/Sdk/DTPipelines/Pipelines/Expressions/IsMatchNode.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class IsMatchNode : FunctionNode
|
||||
{
|
||||
protected sealed override Boolean TraceFullyRealized => false;
|
||||
|
||||
public static Int32 minParameters = 2;
|
||||
public static Int32 maxParameters = 3;
|
||||
|
||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
// isMatch(value: string, regEx: string, options?: string)
|
||||
String value = Parameters[0].EvaluateString(context) ?? String.Empty;
|
||||
String regEx = Parameters[1].EvaluateString(context) ?? String.Empty;
|
||||
String regExOptionsString = String.Empty;
|
||||
|
||||
if (Parameters.Count == 3)
|
||||
{
|
||||
regExOptionsString = Parameters[2].EvaluateString(context) ?? String.Empty;
|
||||
}
|
||||
|
||||
return RegexUtility.IsMatch(value, regEx, regExOptionsString);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Sdk/DTPipelines/Pipelines/Expressions/IsSHA1Node.cs
Normal file
22
src/Sdk/DTPipelines/Pipelines/Expressions/IsSHA1Node.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class IsSHA1Node : FunctionNode
|
||||
{
|
||||
protected sealed override Boolean TraceFullyRealized => false;
|
||||
|
||||
public static Int32 minParameters = 1;
|
||||
public static Int32 maxParameters = 1;
|
||||
|
||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
// isSha1(value: string)
|
||||
String value = Parameters[0].EvaluateString(context) ?? String.Empty;
|
||||
return RegexUtility.IsMatch(value, WellKnownRegularExpressions.SHA1);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Sdk/DTPipelines/Pipelines/Expressions/IsUrlNode.cs
Normal file
22
src/Sdk/DTPipelines/Pipelines/Expressions/IsUrlNode.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class IsUrlNode : FunctionNode
|
||||
{
|
||||
protected sealed override Boolean TraceFullyRealized => false;
|
||||
|
||||
public static Int32 minParameters = 1;
|
||||
public static Int32 maxParameters = 1;
|
||||
|
||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
// isUrl(value: string)
|
||||
String value = Parameters[0].EvaluateString(context) ?? String.Empty;
|
||||
return RegexUtility.IsMatch(value, WellKnownRegularExpressions.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/Sdk/DTPipelines/Pipelines/Expressions/LengthNode.cs
Normal file
63
src/Sdk/DTPipelines/Pipelines/Expressions/LengthNode.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class LengthNode : FunctionNode
|
||||
{
|
||||
protected sealed override Boolean TraceFullyRealized => false;
|
||||
|
||||
public static Int32 minParameters = 1;
|
||||
public static Int32 maxParameters = 1;
|
||||
|
||||
protected sealed override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
// Length(value: object)
|
||||
var evaluationResult = Parameters[0].Evaluate(context);
|
||||
bool kindNotSupported = false;
|
||||
Int32 length = -1;
|
||||
|
||||
switch (evaluationResult.Kind)
|
||||
{
|
||||
case ValueKind.Array:
|
||||
length = ((JArray)evaluationResult.Value).Count;
|
||||
break;
|
||||
case ValueKind.String:
|
||||
length = ((String)evaluationResult.Value).Length;
|
||||
break;
|
||||
case ValueKind.Object:
|
||||
if (evaluationResult.Value is IReadOnlyDictionary<String, Object>)
|
||||
{
|
||||
length = ((IReadOnlyDictionary<String, Object>)evaluationResult.Value).Count;
|
||||
}
|
||||
else if (evaluationResult.Value is ICollection)
|
||||
{
|
||||
length = ((ICollection)evaluationResult.Value).Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
kindNotSupported = true;
|
||||
}
|
||||
break;
|
||||
case ValueKind.Boolean:
|
||||
case ValueKind.Null:
|
||||
case ValueKind.Number:
|
||||
case ValueKind.Version:
|
||||
kindNotSupported = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (kindNotSupported)
|
||||
{
|
||||
throw new NotSupportedException(PipelineStrings.InvalidTypeForLengthFunction(evaluationResult.Kind));
|
||||
}
|
||||
|
||||
return new Decimal(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal sealed class PipelineContextNode : NamedValueNode
|
||||
{
|
||||
protected override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
var state = context.State as IPipelineContext;
|
||||
var result = new Dictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// startTime
|
||||
if (state.Variables.TryGetValue(WellKnownDistributedTaskVariables.PipelineStartTime, out VariableValue startTimeVariable) &&
|
||||
!String.IsNullOrEmpty(startTimeVariable.Value))
|
||||
{
|
||||
// Leverage the expression SDK to convert to datetime
|
||||
var startTimeResult = EvaluationResult.CreateIntermediateResult(context, startTimeVariable.Value, out _);
|
||||
if (startTimeResult.TryConvertToDateTime(context, out DateTimeOffset startTime))
|
||||
{
|
||||
result["startTime"] = startTime;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/Sdk/DTPipelines/Pipelines/Expressions/RegexUtility.cs
Normal file
153
src/Sdk/DTPipelines/Pipelines/Expressions/RegexUtility.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
public static class RegexUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets default timeout for regex
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static TimeSpan GetRegexTimeOut()
|
||||
{
|
||||
return s_regexTimeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs regex single match with ECMAScript-complaint behavior
|
||||
/// Will throw RegularExpressionFailureException if regular expression parsing error occurs or if regular expression takes more than allotted time to execute
|
||||
/// Supported regex options - 'i' (ignorecase), 'm' (multiline)
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="regex"></param>
|
||||
/// <param name="regexOptionsString"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsMatch(
|
||||
String value,
|
||||
String regexPattern,
|
||||
String regexOptionsString)
|
||||
{
|
||||
return IsSafeMatch(value, regexPattern, ConvertToRegexOptions(regexOptionsString));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs regex single match with ECMAScript-complaint behavior
|
||||
/// Will throw RegularExpressionFailureException if regular expression parsing error occurs or if regular expression takes more than allotted time to execute
|
||||
/// If the key is not known, returns true
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="wellKnownRegexKey">One of WellKnownRegularExpressionKeys</param>
|
||||
/// <returns></returns>
|
||||
public static bool IsMatch(
|
||||
String value,
|
||||
String wellKnownRegexKey)
|
||||
{
|
||||
Lazy<Regex> lazyRegex = WellKnownRegularExpressions.GetRegex(wellKnownRegexKey);
|
||||
if (lazyRegex == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Regex regex = lazyRegex.Value;
|
||||
return IsSafeMatch(value, x => regex.Match(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts regex in string to RegExOptions, valid flags are "i", "m"
|
||||
/// Throws RegularExpressionInvalidOptionsException if there are any invalid options
|
||||
/// </summary>
|
||||
/// <param name="regexOptions"></param>
|
||||
/// <returns></returns>
|
||||
public static RegexOptions ConvertToRegexOptions(String regexOptions)
|
||||
{
|
||||
RegexOptions result;
|
||||
if (TryConvertToRegexOptions(regexOptions, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new RegularExpressionInvalidOptionsException(PipelineStrings.InvalidRegexOptions(regexOptions, String.Join(",", WellKnownRegexOptions.All)));
|
||||
}
|
||||
|
||||
private static bool TryConvertToRegexOptions(
|
||||
String regexOptions,
|
||||
out RegexOptions result)
|
||||
{
|
||||
// Eg: "IgnoreCase, MultiLine" or "IgnoreCase"
|
||||
result = RegexOptions.ECMAScript | RegexOptions.CultureInvariant;
|
||||
|
||||
if (String.IsNullOrEmpty(regexOptions))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] regexOptionValues = regexOptions.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
for (int i = 0; i < regexOptionValues.Length; i++)
|
||||
{
|
||||
String option = regexOptionValues[i];
|
||||
|
||||
if (String.Equals(option, WellKnownRegexOptions.IgnoreCase, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = result | RegexOptions.IgnoreCase;
|
||||
}
|
||||
else if (String.Equals(option, WellKnownRegexOptions.Multiline, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = result | RegexOptions.Multiline;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Boolean IsSafeMatch(
|
||||
String value,
|
||||
Func<String, Match> getSafeMatch)
|
||||
{
|
||||
Boolean result = true;
|
||||
try
|
||||
{
|
||||
var match = getSafeMatch(value);
|
||||
result = match.Success;
|
||||
}
|
||||
catch (Exception ex) when (ex is RegexMatchTimeoutException || ex is ArgumentException)
|
||||
{
|
||||
throw new RegularExpressionValidationFailureException(PipelineStrings.RegexFailed(value, ex.Message), ex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Boolean IsSafeMatch(
|
||||
String value,
|
||||
String regex,
|
||||
RegexOptions regexOptions)
|
||||
{
|
||||
return IsSafeMatch(value, x => GetSafeMatch(x, regex, regexOptions));
|
||||
}
|
||||
|
||||
private static Match GetSafeMatch(
|
||||
String value,
|
||||
String regex,
|
||||
RegexOptions regexOptions)
|
||||
{
|
||||
return Regex.Match(value, regex, regexOptions, s_regexTimeout);
|
||||
}
|
||||
|
||||
// 2 seconds should be enough mostly, per DataAnnotations class - http://index/?query=REGEX_DEFAULT_MATCH_TIMEOUT
|
||||
private static TimeSpan s_regexTimeout = TimeSpan.FromSeconds(2);
|
||||
|
||||
private static class WellKnownRegexOptions
|
||||
{
|
||||
public static String IgnoreCase = nameof(IgnoreCase);
|
||||
public static String Multiline = nameof(Multiline);
|
||||
public static String[] All = new String[] { IgnoreCase, Multiline };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class VariablesContextNode : NamedValueNode
|
||||
{
|
||||
protected override Object EvaluateCore(EvaluationContext context)
|
||||
{
|
||||
var executionContext = context.State as IPipelineContext;
|
||||
return executionContext.Variables;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines.Expressions
|
||||
{
|
||||
public static class WellKnownRegularExpressions
|
||||
{
|
||||
public const String Email = nameof(Email);
|
||||
public const String IPv4Address = nameof(IPv4Address);
|
||||
public const String SHA1 = nameof(SHA1);
|
||||
public const String Url = nameof(Url);
|
||||
|
||||
/// <summary>
|
||||
/// Returns null if it's not a well-known type
|
||||
/// </summary>
|
||||
/// <param name="regexType"></param>
|
||||
/// <returns></returns>
|
||||
public static Lazy<Regex> GetRegex(String regexType)
|
||||
{
|
||||
switch (regexType)
|
||||
{
|
||||
case Email:
|
||||
return s_validEmail;
|
||||
case IPv4Address:
|
||||
return s_validIPv4Address;
|
||||
case SHA1:
|
||||
return s_validSha1;
|
||||
case Url:
|
||||
return s_validUrl;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// regex from http://index/?leftProject=System.ComponentModel.DataAnnotations&leftSymbol=cmnlm5e7vdio&file=DataAnnotations%5CEmailAddressAttribute.cs&rightSymbol=jfeiathypuap
|
||||
private static readonly Lazy<Regex> s_validEmail = new Lazy<Regex>(() => new Regex(
|
||||
@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut()
|
||||
)
|
||||
);
|
||||
|
||||
// simple check - {1 to 3 digits}.{1 to 3 digits}.{1 to 3 digits}.{1 to 3 digits}
|
||||
private static readonly Lazy<Regex> s_validIPv4Address = new Lazy<Regex>(() => new Regex(
|
||||
@"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut()
|
||||
)
|
||||
);
|
||||
|
||||
// 40 hex characters
|
||||
private static readonly Lazy<Regex> s_validSha1 = new Lazy<Regex>(() => new Regex(
|
||||
@"\b[0-9a-f]{40}\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut()
|
||||
)
|
||||
);
|
||||
|
||||
// regex from http://index/?leftProject=System.ComponentModel.DataAnnotations&leftSymbol=gk29yrysvq6y&file=DataAnnotations%5CUrlAttribute.cs&line=11
|
||||
private static readonly Lazy<Regex> s_validUrl = new Lazy<Regex>(() => new Regex(
|
||||
@"^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
247
src/Sdk/DTPipelines/Pipelines/GraphCondition.cs
Normal file
247
src/Sdk/DTPipelines/Pipelines/GraphCondition.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.Expressions;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public abstract class GraphCondition<TInstance> where TInstance : IGraphNodeInstance
|
||||
{
|
||||
private protected GraphCondition(String condition)
|
||||
{
|
||||
m_condition = !String.IsNullOrEmpty(condition) ? condition : Default;
|
||||
m_parser = new ExpressionParser();
|
||||
m_parsedCondition = m_parser.CreateTree(m_condition, new ConditionTraceWriter(), s_namedValueInfo, FunctionInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default condition if none is specified
|
||||
/// </summary>
|
||||
public static String Default
|
||||
{
|
||||
get
|
||||
{
|
||||
return $"{PipelineTemplateConstants.Success}()";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the event payload is used within the condition
|
||||
/// </summary>
|
||||
public Boolean RequiresEventPayload
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckRequiredProperties();
|
||||
return m_requiresEventPayload.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether dependency outputs are used within the condition
|
||||
/// </summary>
|
||||
public Boolean RequiresOutputs
|
||||
{
|
||||
get
|
||||
{
|
||||
CheckRequiredProperties();
|
||||
return m_requiresOutputs.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether variables are used within the condition
|
||||
/// </summary>
|
||||
public Boolean RequiresVariables
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckRequiredProperties()
|
||||
{
|
||||
var matches = m_parsedCondition.CheckReferencesContext(PipelineTemplateConstants.EventPattern, PipelineTemplateConstants.OutputsPattern);
|
||||
m_requiresEventPayload = matches[0];
|
||||
m_requiresOutputs = matches[1];
|
||||
}
|
||||
|
||||
private static IEnumerable<DictionaryContextData> GetNeeds(
|
||||
IReadOnlyList<ExpressionNode> parameters,
|
||||
EvaluationContext context,
|
||||
GraphExecutionContext<TInstance> expressionContext)
|
||||
{
|
||||
if (expressionContext.Data.TryGetValue(PipelineTemplateConstants.Needs, out var needsData) &&
|
||||
needsData is DictionaryContextData needs)
|
||||
{
|
||||
if (parameters.Count == 0)
|
||||
{
|
||||
foreach (var pair in needs)
|
||||
{
|
||||
yield return pair.Value as DictionaryContextData;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var parameterResult = parameter.Evaluate(context);
|
||||
var dependencyName = default(String);
|
||||
if (parameterResult.IsPrimitive)
|
||||
{
|
||||
dependencyName = parameterResult.ConvertToString();
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(dependencyName) &&
|
||||
needs.TryGetValue(dependencyName, out var need))
|
||||
{
|
||||
yield return need as DictionaryContextData;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly String m_condition;
|
||||
private readonly ExpressionParser m_parser;
|
||||
private Boolean? m_requiresEventPayload;
|
||||
private Boolean? m_requiresOutputs;
|
||||
protected readonly IExpressionNode m_parsedCondition;
|
||||
|
||||
private static readonly INamedValueInfo[] s_namedValueInfo = new INamedValueInfo[]
|
||||
{
|
||||
new NamedValueInfo<GraphConditionNamedValue<TInstance>>(PipelineTemplateConstants.GitHub),
|
||||
new NamedValueInfo<GraphConditionNamedValue<TInstance>>(PipelineTemplateConstants.Needs),
|
||||
};
|
||||
|
||||
public static readonly IFunctionInfo[] FunctionInfo = new IFunctionInfo[]
|
||||
{
|
||||
new FunctionInfo<AlwaysFunction>(PipelineTemplateConstants.Always, 0, 0),
|
||||
new FunctionInfo<FailureFunction>(PipelineTemplateConstants.Failure, 0, Int32.MaxValue),
|
||||
new FunctionInfo<CancelledFunction>(PipelineTemplateConstants.Cancelled, 0, 0),
|
||||
new FunctionInfo<SuccessFunction>(PipelineTemplateConstants.Success, 0, Int32.MaxValue),
|
||||
};
|
||||
|
||||
protected sealed class ConditionTraceWriter : ITraceWriter
|
||||
{
|
||||
public String Trace
|
||||
{
|
||||
get
|
||||
{
|
||||
return m_info.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public void Info(String message)
|
||||
{
|
||||
m_info.AppendLine(message);
|
||||
}
|
||||
|
||||
public void Verbose(String message)
|
||||
{
|
||||
// Not interested
|
||||
}
|
||||
|
||||
private StringBuilder m_info = new StringBuilder();
|
||||
}
|
||||
|
||||
private sealed class AlwaysFunction : Function
|
||||
{
|
||||
protected override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CancelledFunction : Function
|
||||
{
|
||||
protected override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var conditionContext = context.State as GraphExecutionContext<TInstance>;
|
||||
return conditionContext.State == PipelineState.Canceling;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FailureFunction : Function
|
||||
{
|
||||
protected override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var conditionContext = context.State as GraphExecutionContext<TInstance>;
|
||||
if (conditionContext.State != PipelineState.InProgress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean anyFailed = false;
|
||||
foreach (var need in GetNeeds(Parameters, context, conditionContext))
|
||||
{
|
||||
if (need == null ||
|
||||
!need.TryGetValue(PipelineTemplateConstants.Result, out var resultData) ||
|
||||
!(resultData is StringContextData resultString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String.Equals(resultString, PipelineTemplateConstants.Failure, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
anyFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return anyFailed;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SuccessFunction : Function
|
||||
{
|
||||
protected override Object EvaluateCore(
|
||||
EvaluationContext context,
|
||||
out ResultMemory resultMemory)
|
||||
{
|
||||
resultMemory = null;
|
||||
var conditionContext = context.State as GraphExecutionContext<TInstance>;
|
||||
if (conditionContext.State != PipelineState.InProgress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean allSucceeded = true;
|
||||
foreach (var need in GetNeeds(Parameters, context, conditionContext))
|
||||
{
|
||||
if (!allSucceeded ||
|
||||
need == null ||
|
||||
!need.TryGetValue(PipelineTemplateConstants.Result, out var resultData) ||
|
||||
!(resultData is StringContextData resultString) ||
|
||||
!String.Equals(resultString, PipelineTemplateConstants.Success, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Sdk/DTPipelines/Pipelines/GroupStep.cs
Normal file
86
src/Sdk/DTPipelines/Pipelines/GroupStep.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class GroupStep : JobStep
|
||||
{
|
||||
[JsonConstructor]
|
||||
public GroupStep()
|
||||
{
|
||||
}
|
||||
|
||||
private GroupStep(GroupStep groupStepToClone)
|
||||
: base(groupStepToClone)
|
||||
{
|
||||
if (groupStepToClone.m_steps?.Count > 0)
|
||||
{
|
||||
foreach (var step in groupStepToClone.m_steps)
|
||||
{
|
||||
this.Steps.Add(step.Clone() as TaskStep);
|
||||
}
|
||||
}
|
||||
|
||||
if (groupStepToClone.m_outputs?.Count > 0)
|
||||
{
|
||||
this.m_outputs = new Dictionary<String, String>(groupStepToClone.m_outputs, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public override StepType Type => StepType.Group;
|
||||
|
||||
public IList<TaskStep> Steps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_steps == null)
|
||||
{
|
||||
m_steps = new List<TaskStep>();
|
||||
}
|
||||
return m_steps;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<String, String> Outputs
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_outputs == null)
|
||||
{
|
||||
m_outputs = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
return m_outputs;
|
||||
}
|
||||
}
|
||||
|
||||
public override Step Clone()
|
||||
{
|
||||
return new GroupStep(this);
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_steps?.Count == 0)
|
||||
{
|
||||
m_steps = null;
|
||||
}
|
||||
|
||||
if (m_outputs?.Count == 0)
|
||||
{
|
||||
m_outputs = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "Steps", EmitDefaultValue = false)]
|
||||
private IList<TaskStep> m_steps;
|
||||
|
||||
[DataMember(Name = "Outputs", EmitDefaultValue = false)]
|
||||
private IDictionary<String, String> m_outputs;
|
||||
}
|
||||
}
|
||||
37
src/Sdk/DTPipelines/Pipelines/IAgentPoolResolver.cs
Normal file
37
src/Sdk/DTPipelines/Pipelines/IAgentPoolResolver.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism of resolving an <c>AgentPoolReference</c> to a <c>TaskAgentPool</c>.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IAgentPoolResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve the agent pool references to <c>TaskAgentPool</c> instances.
|
||||
/// </summary>
|
||||
/// <param name="references">The agent pools which should be resolved</param>
|
||||
/// <returns>A list containing the resolved agent pools</returns>
|
||||
IList<TaskAgentPool> Resolve(ICollection<AgentPoolReference> references);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class IAgentPoolResolverExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve the agent pool reference to a <c>TaskAgentPool</c>.
|
||||
/// </summary>
|
||||
/// <param name="reference">The agent pool which should be resolved</param>
|
||||
/// <returns>The agent pool if resolved; otherwise, null</returns>
|
||||
public static TaskAgentPool Resolve(
|
||||
this IAgentPoolResolver resolver,
|
||||
AgentPoolReference reference)
|
||||
{
|
||||
return resolver.Resolve(new[] { reference }).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Sdk/DTPipelines/Pipelines/IAgentPoolStore.cs
Normal file
27
src/Sdk/DTPipelines/Pipelines/IAgentPoolStore.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IAgentPoolStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a reference which should be considered authorized. Future
|
||||
/// calls to retrieve this resource will be treated as pre-authorized regardless
|
||||
/// of authorization context used.
|
||||
/// </summary>
|
||||
/// <param name="pools">The pools which should be authorized</param>
|
||||
void Authorize(IList<AgentPoolReference> pools);
|
||||
|
||||
IList<AgentPoolReference> GetAuthorizedReferences();
|
||||
|
||||
TaskAgentPool Get(AgentPoolReference reference);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <c>IAgentPoolResolver</c> used by this store, if any.
|
||||
/// </summary>
|
||||
IAgentPoolResolver Resolver { get; }
|
||||
}
|
||||
}
|
||||
37
src/Sdk/DTPipelines/Pipelines/IAgentQueueResolver.cs
Normal file
37
src/Sdk/DTPipelines/Pipelines/IAgentQueueResolver.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism of resolving an <c>AgentQueueReference</c> to a <c>TaskAgentQueue</c>.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IAgentQueueResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve the agent queue references to <c>TaskAgentQueue</c> instances.
|
||||
/// </summary>
|
||||
/// <param name="references">The agent queues which should be resolved</param>
|
||||
/// <returns>A list containing the resolved agent queues</returns>
|
||||
IList<TaskAgentQueue> Resolve(ICollection<AgentQueueReference> references);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class IAgentQueueResolverExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve the agent queue reference to a <c>TaskAgentQueue</c>.
|
||||
/// </summary>
|
||||
/// <param name="reference">The agent queue which should be resolved</param>
|
||||
/// <returns>The agent queue if resolved; otherwise, null</returns>
|
||||
public static TaskAgentQueue Resolve(
|
||||
this IAgentQueueResolver resolver,
|
||||
AgentQueueReference reference)
|
||||
{
|
||||
return resolver.Resolve(new[] { reference }).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Sdk/DTPipelines/Pipelines/IAgentQueueStore.cs
Normal file
27
src/Sdk/DTPipelines/Pipelines/IAgentQueueStore.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IAgentQueueStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a reference which should be considered authorized. Future
|
||||
/// calls to retrieve this resource will be treated as pre-authorized regardless
|
||||
/// of authorization context used.
|
||||
/// </summary>
|
||||
/// <param name="reference">The queue which should be authorized</param>
|
||||
void Authorize(IList<TaskAgentQueue> queues);
|
||||
|
||||
IList<AgentQueueReference> GetAuthorizedReferences();
|
||||
|
||||
TaskAgentQueue Get(AgentQueueReference reference);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <c>IAgentQueueResolver</c> used by this store, if any.
|
||||
/// </summary>
|
||||
IAgentQueueResolver Resolver { get; }
|
||||
}
|
||||
}
|
||||
11
src/Sdk/DTPipelines/Pipelines/ICounterResolver.cs
Normal file
11
src/Sdk/DTPipelines/Pipelines/ICounterResolver.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ICounterResolver
|
||||
{
|
||||
Int32 Increment(IPipelineContext context, String prefix, Int32 seed);
|
||||
}
|
||||
}
|
||||
24
src/Sdk/DTPipelines/Pipelines/ICounterStore.cs
Normal file
24
src/Sdk/DTPipelines/Pipelines/ICounterStore.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ICounterStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the counters which are allocated for this store.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<String, Int32> Counters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Increments the counter with the given prefix. If no such counter exists, a new one will be created with
|
||||
/// <paramref name="seed"/> as the initial value.
|
||||
/// </summary>
|
||||
/// <param name="prefix">The counter prefix</param>
|
||||
/// <param name="seed">The initial value for the counter if the counter does not exist</param>
|
||||
/// <returns>The incremented value</returns>
|
||||
Int32 Increment(IPipelineContext context, String prefix, Int32 seed);
|
||||
}
|
||||
}
|
||||
14
src/Sdk/DTPipelines/Pipelines/IEnvironmentResolver.cs
Normal file
14
src/Sdk/DTPipelines/Pipelines/IEnvironmentResolver.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IEnvironmentResolver
|
||||
{
|
||||
EnvironmentInstance Resolve(String environmentName);
|
||||
|
||||
EnvironmentInstance Resolve(Int32 environmentId);
|
||||
}
|
||||
}
|
||||
22
src/Sdk/DTPipelines/Pipelines/IEnvironmentStore.cs
Normal file
22
src/Sdk/DTPipelines/Pipelines/IEnvironmentStore.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a contract for resolving environment from a given store.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IEnvironmentStore
|
||||
{
|
||||
EnvironmentInstance ResolveEnvironment(String environmentName);
|
||||
|
||||
EnvironmentInstance ResolveEnvironment(Int32 environmentId);
|
||||
|
||||
EnvironmentInstance Get(EnvironmentReference reference);
|
||||
|
||||
IList<EnvironmentReference> GetReferences();
|
||||
}
|
||||
}
|
||||
51
src/Sdk/DTPipelines/Pipelines/IGraphNode.cs
Normal file
51
src/Sdk/DTPipelines/Pipelines/IGraphNode.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Pipelines.Validation;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IGraphNode
|
||||
{
|
||||
String Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
String DisplayName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
String Condition
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
ISet<String> DependsOn
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
void Validate(PipelineBuildContext context, ValidationResult result);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IGraphNodeInstance
|
||||
{
|
||||
Int32 Attempt { get; set; }
|
||||
String Identifier { get; set; }
|
||||
String Name { get; set; }
|
||||
DateTime? StartTime { get; set; }
|
||||
DateTime? FinishTime { get; set; }
|
||||
TaskResult? Result { get; set; }
|
||||
Boolean SecretsAccessed { get; }
|
||||
IDictionary<String, VariableValue> Outputs { get; }
|
||||
void ResetSecretsAccessed();
|
||||
}
|
||||
}
|
||||
20
src/Sdk/DTPipelines/Pipelines/IJobFactory.cs
Normal file
20
src/Sdk/DTPipelines/Pipelines/IJobFactory.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
internal interface IJobFactory
|
||||
{
|
||||
String Name { get; }
|
||||
|
||||
Job CreateJob(
|
||||
JobExecutionContext context,
|
||||
ExpressionValue<String> container,
|
||||
IDictionary<String, ExpressionValue<String>> sidecarContainers,
|
||||
Boolean continueOnError,
|
||||
Int32 timeoutInMinutes,
|
||||
Int32 cancelTimeoutInMinutes,
|
||||
String displayName = null);
|
||||
}
|
||||
}
|
||||
12
src/Sdk/DTPipelines/Pipelines/IPackageStore.cs
Normal file
12
src/Sdk/DTPipelines/Pipelines/IPackageStore.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IPackageStore
|
||||
{
|
||||
PackageVersion GetLatestVersion(String packageType);
|
||||
}
|
||||
}
|
||||
20
src/Sdk/DTPipelines/Pipelines/IPhaseProvider.cs
Normal file
20
src/Sdk/DTPipelines/Pipelines/IPhaseProvider.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using GitHub.DistributedTask.Pipelines.Validation;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a temprary extension point for provider phase to participate in pipeline resource discover
|
||||
/// This extension point can be removed after we have the schema driven resource discover
|
||||
/// </summary>
|
||||
public interface IPhaseProvider
|
||||
{
|
||||
String Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Validate pipeline with builder context to provide additional validation errors
|
||||
/// and pipeline resource discover.
|
||||
/// </summary>
|
||||
ValidationResult Validate(PipelineBuildContext context, ProviderPhase phase);
|
||||
}
|
||||
}
|
||||
59
src/Sdk/DTPipelines/Pipelines/IPipelineContext.cs
Normal file
59
src/Sdk/DTPipelines/Pipelines/IPipelineContext.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Logging;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the environment and services available during build and execution of a pipeline.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IPipelineContext
|
||||
{
|
||||
ICounterStore CounterStore { get; }
|
||||
|
||||
DictionaryContextData Data { get; }
|
||||
|
||||
Int32 EnvironmentVersion { get; }
|
||||
|
||||
EvaluationOptions ExpressionOptions { get; }
|
||||
|
||||
IPipelineIdGenerator IdGenerator { get; }
|
||||
|
||||
IPackageStore PackageStore { get; }
|
||||
|
||||
PipelineResources ReferencedResources { get; }
|
||||
|
||||
IResourceStore ResourceStore { get; }
|
||||
|
||||
IReadOnlyList<IStepProvider> StepProviders { get; }
|
||||
|
||||
ISecretMasker SecretMasker { get; }
|
||||
|
||||
ITaskStore TaskStore { get; }
|
||||
|
||||
IPipelineTraceWriter Trace { get; }
|
||||
|
||||
ISet<String> SystemVariableNames { get; }
|
||||
|
||||
IDictionary<String, VariableValue> Variables { get; }
|
||||
|
||||
String ExpandVariables(String value, Boolean maskSecrets = false);
|
||||
|
||||
ExpressionResult<T> Evaluate<T>(String expression);
|
||||
|
||||
ExpressionResult<JObject> Evaluate(JObject value);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IPipelineTraceWriter : ITraceWriter
|
||||
{
|
||||
void EnterProperty(String name);
|
||||
void LeaveProperty(String name);
|
||||
}
|
||||
}
|
||||
81
src/Sdk/DTPipelines/Pipelines/IPipelineContextExtensions.cs
Normal file
81
src/Sdk/DTPipelines/Pipelines/IPipelineContextExtensions.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.Services.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class IPipelineContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Uses the current context to validate the steps provided.
|
||||
/// </summary>
|
||||
/// <param name="context">The current pipeline context</param>
|
||||
/// <param name="steps">The list of steps which should be validated</param>
|
||||
/// <param name="options">The options controlling the level of validation performed</param>
|
||||
/// <returns>A list of validation errors which were encountered, if any</returns>
|
||||
public static IList<PipelineValidationError> Validate(
|
||||
this IPipelineContext context,
|
||||
IList<Step> steps,
|
||||
PhaseTarget target,
|
||||
BuildOptions options)
|
||||
{
|
||||
var builder = new PipelineBuilder(context);
|
||||
return builder.Validate(steps, target, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a property which is specified as an expression and writes the resulting value to the
|
||||
/// corresponding trace log if one is specified on the context.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The result type of the expression</typeparam>
|
||||
/// <param name="context">The pipeline context</param>
|
||||
/// <param name="name">The name of the property being evaluated</param>
|
||||
/// <param name="expression">The expression which should be evaluated</param>
|
||||
/// <param name="defaultValue">The default value if no expression is specified</param>
|
||||
/// <param name="traceDefault">True to write the default value if no expression is specified; otherwise, false</param>
|
||||
/// <returns>The result of the expression evaluation</returns>
|
||||
internal static ExpressionResult<T> Evaluate<T>(
|
||||
this IPipelineContext context,
|
||||
String name,
|
||||
ExpressionValue<T> expression,
|
||||
T defaultValue,
|
||||
Boolean traceDefault = true)
|
||||
{
|
||||
ExpressionResult<T> result = null;
|
||||
if (expression != null)
|
||||
{
|
||||
if (expression.IsLiteral)
|
||||
{
|
||||
context.Trace?.Info($"{name}: {GetTraceValue(expression.Literal)}");
|
||||
result = new ExpressionResult<T>(expression.Literal);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Trace?.EnterProperty(name);
|
||||
result = expression.GetValue(context);
|
||||
context.Trace?.LeaveProperty(name);
|
||||
}
|
||||
}
|
||||
else if (traceDefault && context.Trace != null)
|
||||
{
|
||||
context.Trace.Info($"{name}: {defaultValue}");
|
||||
}
|
||||
|
||||
return result ?? new ExpressionResult<T>(defaultValue);
|
||||
}
|
||||
|
||||
private static String GetTraceValue<T>(T value)
|
||||
{
|
||||
if (value.GetType().IsValueType)
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{System.Environment.NewLine}{JsonUtility.ToString(value, indent: true)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Sdk/DTPipelines/Pipelines/IPipelineIdGenerator.cs
Normal file
35
src/Sdk/DTPipelines/Pipelines/IPipelineIdGenerator.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IPipelineIdGenerator
|
||||
{
|
||||
Guid GetInstanceId(params String[] segments);
|
||||
|
||||
String GetInstanceName(params String[] segments);
|
||||
|
||||
String GetStageIdentifier(String stageName);
|
||||
|
||||
Guid GetStageInstanceId(String stageName, Int32 attempt);
|
||||
|
||||
String GetStageInstanceName(String stageName, Int32 attempt);
|
||||
|
||||
String GetPhaseIdentifier(String stageName, String phaseName);
|
||||
|
||||
Guid GetPhaseInstanceId(String stageName, String phaseName, Int32 attempt);
|
||||
|
||||
String GetPhaseInstanceName(String stageName, String phaseName, Int32 attempt);
|
||||
|
||||
String GetJobIdentifier(String stageName, String phaseName, String jobName);
|
||||
|
||||
Guid GetJobInstanceId(String stageName, String phaseName, String jobName, Int32 attempt);
|
||||
|
||||
String GetJobInstanceName(String stageName, String phaseName, String jobName, Int32 attempt);
|
||||
|
||||
Guid GetTaskInstanceId(String stageName, String phaseName, String jobName, Int32 jobAttempt, String name3);
|
||||
|
||||
String GetTaskInstanceName(String stageName, String phaseName, String jobName, Int32 jobAttempt, String name);
|
||||
}
|
||||
}
|
||||
87
src/Sdk/DTPipelines/Pipelines/IResourceStore.cs
Normal file
87
src/Sdk/DTPipelines/Pipelines/IResourceStore.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.Pipelines.Artifacts;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
public interface IBuildStore : IStepProvider
|
||||
{
|
||||
void Add(BuildResource resource);
|
||||
void Add(IEnumerable<BuildResource> resources);
|
||||
BuildResource Get(String alias);
|
||||
IEnumerable<BuildResource> GetAll();
|
||||
IArtifactResolver Resolver { get; }
|
||||
}
|
||||
|
||||
public interface IContainerStore
|
||||
{
|
||||
void Add(ContainerResource resource);
|
||||
void Add(IEnumerable<ContainerResource> resources);
|
||||
ContainerResource Get(String alias);
|
||||
IEnumerable<ContainerResource> GetAll();
|
||||
}
|
||||
|
||||
public interface IPipelineStore : IStepProvider
|
||||
{
|
||||
void Add(PipelineResource resource);
|
||||
void Add(IEnumerable<PipelineResource> resources);
|
||||
PipelineResource Get(String alias);
|
||||
IEnumerable<PipelineResource> GetAll();
|
||||
}
|
||||
|
||||
public interface IRepositoryStore : IStepProvider
|
||||
{
|
||||
void Add(RepositoryResource resource);
|
||||
void Add(IEnumerable<RepositoryResource> resources);
|
||||
RepositoryResource Get(String alias);
|
||||
IEnumerable<RepositoryResource> GetAll();
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IResourceStore : IStepProvider
|
||||
{
|
||||
IBuildStore Builds { get; }
|
||||
|
||||
IContainerStore Containers { get; }
|
||||
|
||||
IServiceEndpointStore Endpoints { get; }
|
||||
|
||||
ISecureFileStore Files { get; }
|
||||
|
||||
IEnvironmentStore Environments { get; }
|
||||
|
||||
IPipelineStore Pipelines { get; }
|
||||
|
||||
IAgentQueueStore Queues { get; }
|
||||
|
||||
IAgentPoolStore Pools { get; }
|
||||
|
||||
IRepositoryStore Repositories { get; }
|
||||
|
||||
IVariableGroupStore VariableGroups { get; }
|
||||
|
||||
PipelineResources GetAuthorizedResources();
|
||||
|
||||
ServiceEndpoint GetEndpoint(Guid endpointId);
|
||||
|
||||
ServiceEndpoint GetEndpoint(String endpointId);
|
||||
|
||||
SecureFile GetFile(Guid fileId);
|
||||
|
||||
SecureFile GetFile(String fileId);
|
||||
|
||||
TaskAgentQueue GetQueue(Int32 queueId);
|
||||
|
||||
TaskAgentQueue GetQueue(String queueId);
|
||||
|
||||
TaskAgentPool GetPool(Int32 poolId);
|
||||
|
||||
TaskAgentPool GetPool(String poolName);
|
||||
|
||||
VariableGroup GetVariableGroup(Int32 groupId);
|
||||
|
||||
VariableGroup GetVariableGroup(String groupId);
|
||||
}
|
||||
}
|
||||
198
src/Sdk/DTPipelines/Pipelines/IResourceStoreExtensions.cs
Normal file
198
src/Sdk/DTPipelines/Pipelines/IResourceStoreExtensions.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class IResourceStoreExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts the full resources from the <paramref name="store"/> which are referenced in the
|
||||
/// <paramref name="resources"/> collection.
|
||||
/// </summary>
|
||||
/// <param name="store">The store which contains the resources</param>
|
||||
/// <param name="resources">The resources which should be included with the job</param>
|
||||
/// <returns>A new <c>JobResources</c> instance with the filtered set of resources from the store</returns>
|
||||
public static JobResources GetJobResources(
|
||||
this IResourceStore store,
|
||||
PipelineResources resources)
|
||||
{
|
||||
var jobResources = new JobResources();
|
||||
jobResources.Containers.AddRange(resources.Containers.Select(x => x.Clone()));
|
||||
|
||||
foreach (var endpointRef in resources.Endpoints)
|
||||
{
|
||||
var endpoint = store.Endpoints.Get(endpointRef);
|
||||
if (endpoint != null)
|
||||
{
|
||||
jobResources.Endpoints.Add(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var fileRef in resources.Files)
|
||||
{
|
||||
var file = store.Files.Get(fileRef);
|
||||
if (file != null)
|
||||
{
|
||||
jobResources.SecureFiles.Add(file);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var repository in resources.Repositories)
|
||||
{
|
||||
jobResources.Repositories.Add(store.Repositories.Get(repository.Alias));
|
||||
}
|
||||
|
||||
return jobResources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a service endpoint from the store using the provided reference.
|
||||
/// </summary>
|
||||
/// <param name="store">The resource store which should be queried</param>
|
||||
/// <param name="reference">The service endpoint reference which should be resolved</param>
|
||||
/// <returns>A <c>ServiceEndpoint</c> instance matching the specified reference if found; otherwise, null</returns>
|
||||
public static ServiceEndpoint GetEndpoint(
|
||||
this IResourceStore store,
|
||||
ServiceEndpointReference reference)
|
||||
{
|
||||
return store.Endpoints.Get(reference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a secure file from the store using the provided reference.
|
||||
/// </summary>
|
||||
/// <param name="store">The resource store which should be queried</param>
|
||||
/// <param name="reference">The secure file reference which should be resolved</param>
|
||||
/// <returns>A <c>SecureFile</c> instance matching the specified reference if found; otherwise, null</returns>
|
||||
public static SecureFile GetFile(
|
||||
this IResourceStore store,
|
||||
SecureFileReference reference)
|
||||
{
|
||||
return store.Files.Get(reference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent queue from the store using the provided reference.
|
||||
/// </summary>
|
||||
/// <param name="store">The resource store which should be queried</param>
|
||||
/// <param name="reference">The agent queue reference which should be resolved</param>
|
||||
/// <returns>A <c>TaskAgentQueue</c> instance matching the specified reference if found; otherwise, null</returns>
|
||||
public static TaskAgentQueue GetQueue(
|
||||
this IResourceStore store,
|
||||
AgentQueueReference reference)
|
||||
{
|
||||
return store.Queues.Get(reference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an agent pool from the store using the provided reference.
|
||||
/// </summary>
|
||||
/// <param name="store">The resource store which should be queried</param>
|
||||
/// <param name="reference">The agent pool reference which should be resolved</param>
|
||||
/// <returns>A <c>TaskAgentPool</c> instance matching the specified reference if found; otherwise, null</returns>
|
||||
public static TaskAgentPool GetPool(
|
||||
this IResourceStore store,
|
||||
AgentPoolReference reference)
|
||||
{
|
||||
return store.Pools.Get(reference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a variable group from the store using the provided reference.
|
||||
/// </summary>
|
||||
/// <param name="store">The resource store which should be queried</param>
|
||||
/// <param name="reference">The variable group reference which should be resolved</param>
|
||||
/// <returns>A <c>VariableGroup</c> instance matching the specified reference if found; otherwise, null</returns>
|
||||
public static VariableGroup GetVariableGroup(
|
||||
this IResourceStore store,
|
||||
VariableGroupReference reference)
|
||||
{
|
||||
return store.VariableGroups.Get(reference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a partially formed reference, returns the associated reference stored with the plan.
|
||||
/// </summary>
|
||||
public static ResourceReference GetSnappedReference(
|
||||
this IResourceStore store,
|
||||
ResourceReference r)
|
||||
{
|
||||
if (r is VariableGroupReference vgr)
|
||||
{
|
||||
var m = store.VariableGroups.Get(vgr);
|
||||
if (m != null)
|
||||
{
|
||||
return new VariableGroupReference
|
||||
{
|
||||
Id = m.Id,
|
||||
Name = m.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (r is AgentQueueReference aqr)
|
||||
{
|
||||
var m = store.Queues.Get(aqr);
|
||||
if (m != null)
|
||||
{
|
||||
return new AgentQueueReference
|
||||
{
|
||||
Id = m.Id,
|
||||
Name = m.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (r is AgentPoolReference apr)
|
||||
{
|
||||
var m = store.Pools.Get(apr);
|
||||
if (m != null)
|
||||
{
|
||||
return new AgentPoolReference
|
||||
{
|
||||
Id = m.Id,
|
||||
Name = m.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (r is ServiceEndpointReference ser)
|
||||
{
|
||||
var m = store.Endpoints.Get(ser);
|
||||
if (m != null)
|
||||
{
|
||||
return new ServiceEndpointReference
|
||||
{
|
||||
Id = m.Id,
|
||||
Name = m.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (r is SecureFileReference sfr)
|
||||
{
|
||||
var m = store.Files.Get(sfr);
|
||||
if (m != null)
|
||||
{
|
||||
return new SecureFileReference
|
||||
{
|
||||
Id = m.Id,
|
||||
Name = m.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (r is EnvironmentReference er)
|
||||
{
|
||||
var m = store.Environments.Get(er);
|
||||
if (m != null)
|
||||
{
|
||||
return new EnvironmentReference
|
||||
{
|
||||
Id = m.Id,
|
||||
Name = m.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Sdk/DTPipelines/Pipelines/ISecureFileResolver.cs
Normal file
37
src/Sdk/DTPipelines/Pipelines/ISecureFileResolver.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism of resolving an <c>SecureFileReference</c> to a <c>SecureFile</c>.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ISecureFileResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve secure file references to a <c>SecureFile</c> instances.
|
||||
/// </summary>
|
||||
/// <param name="reference">The file references which should be resolved</param>
|
||||
/// <returns>The resolved secure files</returns>
|
||||
IList<SecureFile> Resolve(ICollection<SecureFileReference> references);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class ISecureFileResolverExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve the secure file reference to a <c>SecureFile</c>.
|
||||
/// </summary>
|
||||
/// <param name="reference">The file reference which should be resolved</param>
|
||||
/// <returns>The secure file if resolved; otherwise, null</returns>
|
||||
public static SecureFile Resolve(
|
||||
this ISecureFileResolver resolver,
|
||||
SecureFileReference reference)
|
||||
{
|
||||
return resolver.Resolve(new[] { reference }).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Sdk/DTPipelines/Pipelines/ISecureFileStore.cs
Normal file
19
src/Sdk/DTPipelines/Pipelines/ISecureFileStore.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ISecureFileStore
|
||||
{
|
||||
IList<SecureFileReference> GetAuthorizedReferences();
|
||||
|
||||
SecureFile Get(SecureFileReference reference);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <c>ISecureFileResolver</c> used by this store, if any.
|
||||
/// </summary>
|
||||
ISecureFileResolver Resolver { get; }
|
||||
}
|
||||
}
|
||||
46
src/Sdk/DTPipelines/Pipelines/IServiceEndpointResolver.cs
Normal file
46
src/Sdk/DTPipelines/Pipelines/IServiceEndpointResolver.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism of resolving an <c>ServiceEndpointReference</c> to a <c>ServiceEndpoint</c>.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IServiceEndpointResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the endpoint reference as authorized to ensure future retrievals of the endpoint
|
||||
/// are allowed regardless of security context.
|
||||
/// </summary>
|
||||
/// <param name="reference">The endpoint reference which should be considered authorized</param>
|
||||
void Authorize(ServiceEndpointReference reference);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve endpoint references to <c>ServiceEndpoint</c> instances.
|
||||
/// </summary>
|
||||
/// <param name="references">The endpoint references which should be resolved</param>
|
||||
/// <returns>The resolved service endpoints</returns>
|
||||
IList<ServiceEndpoint> Resolve(ICollection<ServiceEndpointReference> references);
|
||||
|
||||
IList<ServiceEndpointReference> GetAuthorizedReferences();
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class IServiceEndpointResolverExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve the endpoint reference to a <c>ServiceEndpoint</c>.
|
||||
/// </summary>
|
||||
/// <param name="reference">The endpoint reference which should be resolved</param>
|
||||
/// <returns>The service endpoint if resolved; otherwise, null</returns>
|
||||
public static ServiceEndpoint Resolve(
|
||||
this IServiceEndpointResolver resolver,
|
||||
ServiceEndpointReference reference)
|
||||
{
|
||||
return resolver.Resolve(new[] { reference }).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Sdk/DTPipelines/Pipelines/IServiceEndpointStore.cs
Normal file
39
src/Sdk/DTPipelines/Pipelines/IServiceEndpointStore.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to service endpoints which are referenced within a pipeline.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IServiceEndpointStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the list of all endpoints authorized for use in this store.
|
||||
/// </summary>
|
||||
/// <returns>The list of <c>ServiceEndpointReference</c> objects authorized for use</returns>
|
||||
IList<ServiceEndpointReference> GetAuthorizedReferences();
|
||||
|
||||
/// <summary>
|
||||
/// Adds an endpoint reference which should be considered authorized. Future
|
||||
/// calls to retrieve this resource will be treated as pre-authorized regardless
|
||||
/// of authorization context used.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint which should be authorized</param>
|
||||
void Authorize(ServiceEndpointReference endpoint);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to authorize an endpoint for use.
|
||||
/// </summary>
|
||||
/// <param name="endpoint">The endpoint reference to be resolved</param>
|
||||
/// <returns>The endpoint if found and authorized; otherwise, null</returns>
|
||||
ServiceEndpoint Get(ServiceEndpointReference endpoint);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <c>IServiceEndpointResolver</c> used by this store, if any.
|
||||
/// </summary>
|
||||
IServiceEndpointResolver Resolver { get; }
|
||||
}
|
||||
}
|
||||
21
src/Sdk/DTPipelines/Pipelines/IStepProvider.cs
Normal file
21
src/Sdk/DTPipelines/Pipelines/IStepProvider.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
public interface IStepProvider
|
||||
{
|
||||
IList<TaskStep> GetPreSteps(IPipelineContext context, IReadOnlyList<JobStep> steps);
|
||||
Dictionary<Guid, List<TaskStep>> GetPostTaskSteps(IPipelineContext context, IReadOnlyList<JobStep> steps);
|
||||
IList<TaskStep> GetPostSteps(IPipelineContext context, IReadOnlyList<JobStep> steps);
|
||||
|
||||
/// <summary>
|
||||
/// Given a JobStep (eg., download step) it will translate into corresndponding task steps
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="step">Input step to be resolved</param>
|
||||
/// <param name="resolvedSteps">Resolved output steps</param>
|
||||
/// <returns>true if this is resolved, false otherwise. Passing a powershell step to ResolveStep would return false</returns>
|
||||
Boolean ResolveStep(IPipelineContext context, JobStep step, out IList<TaskStep> resolvedSteps);
|
||||
}
|
||||
}
|
||||
12
src/Sdk/DTPipelines/Pipelines/ITaskResolver.cs
Normal file
12
src/Sdk/DTPipelines/Pipelines/ITaskResolver.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ITaskResolver
|
||||
{
|
||||
TaskDefinition Resolve(Guid taskId, String versionSpec);
|
||||
}
|
||||
}
|
||||
30
src/Sdk/DTPipelines/Pipelines/ITaskStore.cs
Normal file
30
src/Sdk/DTPipelines/Pipelines/ITaskStore.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a contract for resolving tasks from a given store.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ITaskStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a task from the store using the unqiue identifier and version.
|
||||
/// </summary>
|
||||
/// <param name="taskId">The unique identifier of the task</param>
|
||||
/// <param name="version">The version of the task which is desired</param>
|
||||
/// <returns>The closest matching task definition if found; otherwise, null</returns>
|
||||
TaskDefinition ResolveTask(Guid taskId, String version);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a task from the store using the specified name and version.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the task</param>
|
||||
/// <param name="version">The version of the task which is desired</param>
|
||||
/// <returns>The closest matching task definition if found; otherwise, null</returns>
|
||||
TaskDefinition ResolveTask(String name, String version);
|
||||
}
|
||||
}
|
||||
14
src/Sdk/DTPipelines/Pipelines/ITaskTemplateResolver.cs
Normal file
14
src/Sdk/DTPipelines/Pipelines/ITaskTemplateResolver.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ITaskTemplateResolver
|
||||
{
|
||||
Boolean CanResolve(TaskTemplateReference template);
|
||||
|
||||
IList<TaskStep> ResolveTasks(TaskTemplateStep template);
|
||||
}
|
||||
}
|
||||
16
src/Sdk/DTPipelines/Pipelines/ITaskTemplateStore.cs
Normal file
16
src/Sdk/DTPipelines/Pipelines/ITaskTemplateStore.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism for task templates to be resolved at build time.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface ITaskTemplateStore
|
||||
{
|
||||
void AddProvider(ITaskTemplateResolver provider);
|
||||
|
||||
IEnumerable<TaskStep> ResolveTasks(TaskTemplateStep step);
|
||||
}
|
||||
}
|
||||
88
src/Sdk/DTPipelines/Pipelines/IVariable.cs
Normal file
88
src/Sdk/DTPipelines/Pipelines/IVariable.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
public enum VariableType
|
||||
{
|
||||
Inline = 0,
|
||||
Group = 1,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(VariableJsonConverter))]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IVariable
|
||||
{
|
||||
VariableType Type { get; }
|
||||
}
|
||||
|
||||
internal class VariableJsonConverter : VssSecureJsonConverter
|
||||
{
|
||||
public VariableJsonConverter()
|
||||
{
|
||||
}
|
||||
|
||||
public override Boolean CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override Boolean CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(IVariable).IsAssignableFrom(objectType);
|
||||
}
|
||||
|
||||
public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.StartObject)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var resultObj = JObject.Load(reader);
|
||||
var variableType = VariableType.Inline;
|
||||
if (resultObj.TryGetValue("type", StringComparison.OrdinalIgnoreCase, out var rawValue))
|
||||
{
|
||||
if (rawValue.Type == JTokenType.Integer)
|
||||
{
|
||||
variableType = (VariableType)(Int32)rawValue;
|
||||
}
|
||||
if (rawValue.Type == JTokenType.String)
|
||||
{
|
||||
variableType = (VariableType)Enum.Parse(typeof(VariableType), (String)rawValue, true);
|
||||
}
|
||||
}
|
||||
else if (resultObj.TryGetValue("id", StringComparison.OrdinalIgnoreCase, out _) ||
|
||||
resultObj.TryGetValue("groupType", StringComparison.OrdinalIgnoreCase, out _) ||
|
||||
resultObj.TryGetValue("secretStore", StringComparison.OrdinalIgnoreCase, out _))
|
||||
{
|
||||
variableType = VariableType.Group;
|
||||
}
|
||||
|
||||
IVariable result = null;
|
||||
switch (variableType)
|
||||
{
|
||||
case VariableType.Group:
|
||||
result = new VariableGroupReference();
|
||||
break;
|
||||
|
||||
default:
|
||||
result = new Variable();
|
||||
break;
|
||||
}
|
||||
|
||||
using (var objectReader = resultObj.CreateReader())
|
||||
{
|
||||
serializer.Populate(objectReader, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Sdk/DTPipelines/Pipelines/IVariableGroupResolver.cs
Normal file
32
src/Sdk/DTPipelines/Pipelines/IVariableGroupResolver.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a mechanism of resolving an <c>VariableGroupReference</c> to a <c>VariableGroup</c>.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IVariableGroupResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve variable group references to <c>VariableGroup</c> instances.
|
||||
/// </summary>
|
||||
/// <param name="reference">The variable groups which should be resolved</param>
|
||||
/// <returns>The resolved variable groups</returns>
|
||||
IList<VariableGroup> Resolve(ICollection<VariableGroupReference> references);
|
||||
}
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class IVariableGroupResolverExtensions
|
||||
{
|
||||
public static VariableGroup Resolve(
|
||||
this IVariableGroupResolver resolver,
|
||||
VariableGroupReference reference)
|
||||
{
|
||||
return resolver.Resolve(new[] { reference }).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Sdk/DTPipelines/Pipelines/IVariableGroupStore.cs
Normal file
21
src/Sdk/DTPipelines/Pipelines/IVariableGroupStore.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IVariableGroupStore : IStepProvider
|
||||
{
|
||||
IList<VariableGroupReference> GetAuthorizedReferences();
|
||||
|
||||
VariableGroup Get(VariableGroupReference queue);
|
||||
|
||||
IVariableValueProvider GetValueProvider(VariableGroupReference queue);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <c>IVariableGroupsResolver</c> used by this store, if any.
|
||||
/// </summary>
|
||||
IVariableGroupResolver Resolver { get; }
|
||||
}
|
||||
}
|
||||
22
src/Sdk/DTPipelines/Pipelines/IVariableValueProvider.cs
Normal file
22
src/Sdk/DTPipelines/Pipelines/IVariableValueProvider.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public interface IVariableValueProvider
|
||||
{
|
||||
String GroupType
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
Boolean ShouldGetValues(IPipelineContext context);
|
||||
|
||||
IList<TaskStep> GetSteps(IPipelineContext context, VariableGroupReference group, IEnumerable<String> keys);
|
||||
|
||||
IDictionary<String, VariableValue> GetValues(VariableGroup group, ServiceEndpoint endpoint, IEnumerable<String> keys, Boolean includeSecrets);
|
||||
}
|
||||
}
|
||||
291
src/Sdk/DTPipelines/Pipelines/Job.cs
Normal file
291
src/Sdk/DTPipelines/Pipelines/Job.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.Pipelines.Runtime;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class Job
|
||||
{
|
||||
[JsonConstructor]
|
||||
public Job()
|
||||
{
|
||||
}
|
||||
|
||||
private Job(Job jobToCopy)
|
||||
{
|
||||
this.Id = jobToCopy.Id;
|
||||
this.Name = jobToCopy.Name;
|
||||
this.DisplayName = jobToCopy.DisplayName;
|
||||
this.Container = jobToCopy.Container?.Clone();
|
||||
this.ServiceContainers = jobToCopy.ServiceContainers?.Clone();
|
||||
this.ContinueOnError = jobToCopy.ContinueOnError;
|
||||
this.TimeoutInMinutes = jobToCopy.TimeoutInMinutes;
|
||||
this.CancelTimeoutInMinutes = jobToCopy.CancelTimeoutInMinutes;
|
||||
this.Workspace = jobToCopy.Workspace?.Clone();
|
||||
this.Target = jobToCopy.Target?.Clone();
|
||||
this.EnvironmentVariables = jobToCopy.EnvironmentVariables?.Clone();
|
||||
|
||||
if (jobToCopy.m_demands != null && jobToCopy.m_demands.Count > 0)
|
||||
{
|
||||
m_demands = new List<Demand>(jobToCopy.m_demands.Select(x => x.Clone()));
|
||||
}
|
||||
|
||||
if (jobToCopy.m_steps != null && jobToCopy.m_steps.Count > 0)
|
||||
{
|
||||
m_steps = new List<JobStep>(jobToCopy.m_steps.Select(x => x.Clone() as JobStep));
|
||||
}
|
||||
|
||||
if (jobToCopy.m_variables != null && jobToCopy.m_variables.Count > 0)
|
||||
{
|
||||
m_variables = new List<IVariable>(jobToCopy.m_variables);
|
||||
}
|
||||
|
||||
if (jobToCopy.m_sidecarContainers != null && jobToCopy.m_sidecarContainers.Count > 0)
|
||||
{
|
||||
m_sidecarContainers = new Dictionary<String, String>(jobToCopy.m_sidecarContainers, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public Guid Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember]
|
||||
public String Name
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public String DisplayName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken Container
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken ServiceContainers
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Boolean ContinueOnError
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken EnvironmentVariables
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Int32 TimeoutInMinutes
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Int32 CancelTimeoutInMinutes
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public IList<Demand> Demands
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_demands == null)
|
||||
{
|
||||
m_demands = new List<Demand>();
|
||||
}
|
||||
return m_demands;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public IdentityRef ExecuteAs
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public WorkspaceOptions Workspace
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public PhaseTarget Target
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public IList<JobStep> Steps
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_steps == null)
|
||||
{
|
||||
m_steps = new List<JobStep>();
|
||||
}
|
||||
return m_steps;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<ContextScope> Scopes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_scopes == null)
|
||||
{
|
||||
m_scopes = new List<ContextScope>();
|
||||
}
|
||||
return m_scopes;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<String, String> SidecarContainers
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_sidecarContainers == null)
|
||||
{
|
||||
m_sidecarContainers = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
return m_sidecarContainers;
|
||||
}
|
||||
}
|
||||
|
||||
public IList<IVariable> Variables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_variables == null)
|
||||
{
|
||||
m_variables = new List<IVariable>();
|
||||
}
|
||||
return m_variables;
|
||||
}
|
||||
}
|
||||
|
||||
public Job Clone()
|
||||
{
|
||||
return new Job(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a task using the specified execution context.
|
||||
/// </summary>
|
||||
/// <param name="context">The job execution context</param>
|
||||
/// <param name="taskName">The name of the task in the steps list</param>
|
||||
/// <returns></returns>
|
||||
public CreateTaskResult CreateTask(
|
||||
JobExecutionContext context,
|
||||
String taskName)
|
||||
{
|
||||
ArgumentUtility.CheckStringForNullOrEmpty(taskName, nameof(taskName));
|
||||
|
||||
TaskDefinition definition = null;
|
||||
var task = this.Steps.SingleOrDefault(x => taskName.Equals(x.Name, StringComparison.OrdinalIgnoreCase))?.Clone() as TaskStep;
|
||||
if (task != null)
|
||||
{
|
||||
definition = context.TaskStore.ResolveTask(task.Reference.Id, task.Reference.Version);
|
||||
foreach (var input in definition.Inputs.Where(x => x != null))
|
||||
{
|
||||
var key = input.Name?.Trim() ?? String.Empty;
|
||||
if (!String.IsNullOrEmpty(key))
|
||||
{
|
||||
if (!task.Inputs.ContainsKey(key))
|
||||
{
|
||||
task.Inputs[key] = input.DefaultValue?.Trim() ?? String.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now expand any macros which appear in inputs
|
||||
foreach (var input in task.Inputs.ToArray())
|
||||
{
|
||||
task.Inputs[input.Key] = context.ExpandVariables(input.Value);
|
||||
}
|
||||
|
||||
// Set the system variables populated while running an individual task
|
||||
context.Variables[WellKnownDistributedTaskVariables.TaskInstanceId] = task.Id.ToString("D");
|
||||
context.Variables[WellKnownDistributedTaskVariables.TaskDisplayName] = task.DisplayName ?? task.Name;
|
||||
context.Variables[WellKnownDistributedTaskVariables.TaskInstanceName] = task.Name;
|
||||
}
|
||||
|
||||
return new CreateTaskResult(task, definition);
|
||||
}
|
||||
|
||||
[OnSerializing]
|
||||
private void OnSerializing(StreamingContext context)
|
||||
{
|
||||
if (m_demands?.Count == 0)
|
||||
{
|
||||
m_demands = null;
|
||||
}
|
||||
|
||||
if (m_steps?.Count == 0)
|
||||
{
|
||||
m_steps = null;
|
||||
}
|
||||
|
||||
if (m_scopes?.Count == 0)
|
||||
{
|
||||
m_scopes = null;
|
||||
}
|
||||
|
||||
if (m_variables?.Count == 0)
|
||||
{
|
||||
m_variables = null;
|
||||
}
|
||||
}
|
||||
|
||||
[DataMember(Name = "Demands", EmitDefaultValue = false)]
|
||||
private List<Demand> m_demands;
|
||||
|
||||
[DataMember(Name = "Steps", EmitDefaultValue = false)]
|
||||
private List<JobStep> m_steps;
|
||||
|
||||
[DataMember(Name = "Scopes", EmitDefaultValue = false)]
|
||||
private List<ContextScope> m_scopes;
|
||||
|
||||
[DataMember(Name = "Variables", EmitDefaultValue = false)]
|
||||
private List<IVariable> m_variables;
|
||||
|
||||
[DataMember(Name = "SidecarContainers", EmitDefaultValue = false)]
|
||||
private IDictionary<String, String> m_sidecarContainers;
|
||||
}
|
||||
}
|
||||
60
src/Sdk/DTPipelines/Pipelines/JobContainer.cs
Normal file
60
src/Sdk/DTPipelines/Pipelines/JobContainer.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class JobContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Generated unique alias
|
||||
/// </summary>
|
||||
public String Alias { get; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the environment which is provided to the container.
|
||||
/// </summary>
|
||||
public IDictionary<String, String> Environment
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the container image name.
|
||||
/// </summary>
|
||||
public String Image
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the options used for the container instance.
|
||||
/// </summary>
|
||||
public String Options
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the volumes which are mounted into the container.
|
||||
/// </summary>
|
||||
public IList<String> Volumes
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ports which are exposed on the container.
|
||||
/// </summary>
|
||||
public IList<String> Ports
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/Sdk/DTPipelines/Pipelines/JobExpansionOptions.cs
Normal file
106
src/Sdk/DTPipelines/Pipelines/JobExpansionOptions.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
public class JobExpansionOptions
|
||||
{
|
||||
public JobExpansionOptions(ICollection<String> configurations)
|
||||
{
|
||||
AddConfigurations(configurations);
|
||||
}
|
||||
|
||||
internal JobExpansionOptions(IDictionary<String, Int32> configurations)
|
||||
{
|
||||
UpdateConfigurations(configurations);
|
||||
}
|
||||
|
||||
internal JobExpansionOptions(
|
||||
String configuration,
|
||||
Int32 attemptNumber = NoSpecifiedAttemptNumber)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(configuration))
|
||||
{
|
||||
this.Configurations.Add(configuration, attemptNumber);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a filter for the expansion of specific Phase configurations.
|
||||
/// The key is the configuration name, the value is the explicitly requested
|
||||
/// attempt number.
|
||||
/// If mapping is null, there is no filter and all configurations will be
|
||||
/// produced.
|
||||
/// </summary>
|
||||
internal IDictionary<String, Int32> Configurations
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_configurations == null)
|
||||
{
|
||||
m_configurations = new Dictionary<String, Int32>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
return m_configurations;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean IsIncluded(String configuration)
|
||||
{
|
||||
return m_configurations == null || m_configurations.ContainsKey(configuration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add new configurations, with no specified custom attempt number
|
||||
/// </summary>
|
||||
public void AddConfigurations(ICollection<String> configurations)
|
||||
{
|
||||
if (configurations == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var localConfigs = this.Configurations;
|
||||
foreach (var c in configurations)
|
||||
{
|
||||
if (!localConfigs.ContainsKey(c))
|
||||
{
|
||||
localConfigs[c] = NoSpecifiedAttemptNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// add (or replace) any configurations and their associated attempt numbers with new provided values.
|
||||
/// </summary>
|
||||
public void UpdateConfigurations(IDictionary<String, Int32> configurations)
|
||||
{
|
||||
if (configurations == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var localConfigs = this.Configurations;
|
||||
foreach (var pair in configurations)
|
||||
{
|
||||
localConfigs[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns custom attempt number or JobExpansionOptions.NoSpecifiedAttemptNumber if none specified.
|
||||
/// </summary>
|
||||
/// <param name="configuration">configuration or "job name"</param>
|
||||
public Int32 GetAttemptNumber(String configuration)
|
||||
{
|
||||
if (m_configurations != null && m_configurations.TryGetValue(configuration, out Int32 number))
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
return NoSpecifiedAttemptNumber;
|
||||
}
|
||||
|
||||
public const Int32 NoSpecifiedAttemptNumber = -1;
|
||||
private Dictionary<String, Int32> m_configurations;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user