GitHub Actions Runner

This commit is contained in:
Tingluo Huang
2019-10-10 00:52:42 -04:00
commit c8afc84840
1255 changed files with 198670 additions and 0 deletions

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -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();
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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);
}
}

View 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;
}
}

View 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();
}
}
}

View 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);
}
}

View 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();
}
}
}
}

View 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);
}
}
}

View File

@@ -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];
}
}
}
}

View 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);
}
}

View File

@@ -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
}
},
};
}
}

View File

@@ -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";
}
}

View 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;
}
}
}

View 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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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";
}
}

View 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; }
}
}

View 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; }
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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}'");
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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}'");
}
}
}
}

View 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; }
}
}

View File

@@ -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;
}
}

View 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);
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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;
}
}

View 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 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();
}
}
}

View 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; }
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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&lt;T&gt;</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;
}
}
}

View 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);
}
}
}
}
}

View 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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"),
};
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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 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);
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -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;
}
}
}

View 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 };
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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()
)
);
}
}

View 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;
}
}
}
}

View 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;
}
}

View 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();
}
}
}

View 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; }
}
}

View 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();
}
}
}

View 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; }
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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();
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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)}";
}
}
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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; }
}
}

View 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();
}
}
}

View 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; }
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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; }
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}
}

View 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