diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index e820725ca..93cd7ffe7 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -41,6 +41,8 @@ namespace GitHub.Runner.Common public static string PluginTracePrefix = "##[plugin.trace]"; public static readonly int RunnerDownloadRetryMaxAttempts = 3; + public static readonly int CompositeActionsMaxDepth = 9; + // This enum is embedded within the Constants class to make it easier to reference and avoid // ambiguous type reference with System.Runtime.InteropServices.OSPlatform and System.Runtime.InteropServices.Architecture public enum OSPlatform diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index 7da7c7366..54156b8dc 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -53,30 +53,63 @@ namespace GitHub.Runner.Worker public Dictionary CachedActionContainers => _cachedActionContainers; public async Task PrepareActionsAsync(IExecutionContext executionContext, IEnumerable steps) { + // Assert inputs ArgUtil.NotNull(executionContext, nameof(executionContext)); ArgUtil.NotNull(steps, nameof(steps)); - - executionContext.Output("Prepare all required actions"); - Dictionary> imagesToPull = new Dictionary>(StringComparer.OrdinalIgnoreCase); - Dictionary> imagesToBuild = new Dictionary>(StringComparer.OrdinalIgnoreCase); - Dictionary imagesToBuildInfo = new Dictionary(StringComparer.OrdinalIgnoreCase); - List containerSetupSteps = new List(); - Dictionary preStepTracker = new Dictionary(); - IEnumerable actions = steps.OfType(); - - // TODO: Deprecate the PREVIEW_ACTION_TOKEN - // Log even if we aren't using it to ensure users know. - if (!string.IsNullOrEmpty(executionContext.Global.Variables.Get("PREVIEW_ACTION_TOKEN"))) + var state = new PrepareActionsState { - executionContext.Warning("The 'PREVIEW_ACTION_TOKEN' secret is deprecated. Please remove it from the repository's secrets"); + ImagesToBuild = new Dictionary>(StringComparer.OrdinalIgnoreCase), + ImagesToPull = new Dictionary>(StringComparer.OrdinalIgnoreCase), + ImagesToBuildInfo = new Dictionary(StringComparer.OrdinalIgnoreCase), + PreStepTracker = new Dictionary() + }; + var containerSetupSteps = new List(); + IOUtil.DeleteDirectory(HostContext.GetDirectory(WellKnownDirectory.Actions), executionContext.CancellationToken); + IEnumerable actions = steps.OfType(); + executionContext.Output("Prepare all required actions"); + var result = await PrepareActionsRecursiveAsync(executionContext, state, actions, 0); + if (state.ImagesToPull.Count > 0) + { + foreach (var imageToPull in result.ImagesToPull) + { + Trace.Info($"{imageToPull.Value.Count} steps need to pull image '{imageToPull.Key}'"); + containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.PullActionContainerAsync, + condition: $"{PipelineTemplateConstants.Success}()", + displayName: $"Pull {imageToPull.Key}", + data: new ContainerSetupInfo(imageToPull.Value, imageToPull.Key))); + } } - // Clear the cache (for self-hosted runners) - IOUtil.DeleteDirectory(HostContext.GetDirectory(WellKnownDirectory.Actions), executionContext.CancellationToken); + if (result.ImagesToBuild.Count > 0) + { + foreach (var imageToBuild in result.ImagesToBuild) + { + var setupInfo = result.ImagesToBuildInfo[imageToBuild.Key]; + Trace.Info($"{imageToBuild.Value.Count} steps need to build image from '{setupInfo.Dockerfile}'"); + containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.BuildActionContainerAsync, + condition: $"{PipelineTemplateConstants.Success}()", + displayName: $"Build {setupInfo.ActionRepository}", + data: new ContainerSetupInfo(imageToBuild.Value, setupInfo.Dockerfile, setupInfo.WorkingDirectory))); + } + } - // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed - var newActionMetadata = executionContext.Global.Variables.GetBoolean("DistributedTask.NewActionMetadata") ?? false; +#if !OS_LINUX + if (containerSetupSteps.Count > 0) + { + executionContext.Output("Container action is only supported on Linux, skip pull and build docker images."); + containerSetupSteps.Clear(); + } +#endif + return new PrepareResult(containerSetupSteps, result.PreStepTracker); + } + private async Task PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable actions, Int32 depth = 0) + { + ArgUtil.NotNull(executionContext, nameof(executionContext)); + if (depth > Constants.CompositeActionsMaxDepth) + { + throw new Exception($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}"); + } var repositoryActions = new List(); foreach (var action in actions) @@ -88,66 +121,15 @@ namespace GitHub.Runner.Worker ArgUtil.NotNull(containerReference, nameof(containerReference)); ArgUtil.NotNullOrEmpty(containerReference.Image, nameof(containerReference.Image)); - if (!imagesToPull.ContainsKey(containerReference.Image)) + if (!state.ImagesToPull.ContainsKey(containerReference.Image)) { - imagesToPull[containerReference.Image] = new List(); + state.ImagesToPull[containerReference.Image] = new List(); } Trace.Info($"Action {action.Name} ({action.Id}) needs to pull image '{containerReference.Image}'"); - imagesToPull[containerReference.Image].Add(action.Id); + state.ImagesToPull[containerReference.Image].Add(action.Id); } - // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed - else if (action.Reference.Type == Pipelines.ActionSourceType.Repository && !newActionMetadata) - { - // only download the repository archive - await DownloadRepositoryActionAsync(executionContext, action); - - // more preparation base on content in the repository (action.yml) - var setupInfo = PrepareRepositoryActionAsync(executionContext, action); - if (setupInfo != null) - { - if (!string.IsNullOrEmpty(setupInfo.Image)) - { - if (!imagesToPull.ContainsKey(setupInfo.Image)) - { - imagesToPull[setupInfo.Image] = new List(); - } - - Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to pull image '{setupInfo.Image}'"); - imagesToPull[setupInfo.Image].Add(action.Id); - } - else - { - ArgUtil.NotNullOrEmpty(setupInfo.ActionRepository, nameof(setupInfo.ActionRepository)); - - if (!imagesToBuild.ContainsKey(setupInfo.ActionRepository)) - { - imagesToBuild[setupInfo.ActionRepository] = new List(); - } - - Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to build image '{setupInfo.Dockerfile}'"); - imagesToBuild[setupInfo.ActionRepository].Add(action.Id); - imagesToBuildInfo[setupInfo.ActionRepository] = setupInfo; - } - } - - var repoAction = action.Reference as Pipelines.RepositoryPathReference; - if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias) - { - var definition = LoadAction(executionContext, action); - if (definition.Data.Execution.HasPre) - { - var actionRunner = HostContext.CreateService(); - actionRunner.Action = action; - actionRunner.Stage = ActionRunStage.Pre; - actionRunner.Condition = definition.Data.Execution.InitCondition; - - Trace.Info($"Add 'pre' execution for {action.Id}"); - preStepTracker[action.Id] = actionRunner; - } - } - } - else if (action.Reference.Type == Pipelines.ActionSourceType.Repository && newActionMetadata) + else if (action.Reference.Type == Pipelines.ActionSourceType.Repository) { repositoryActions.Add(action); } @@ -179,38 +161,42 @@ namespace GitHub.Runner.Worker foreach (var action in repositoryActions) { var setupInfo = PrepareRepositoryActionAsync(executionContext, action); - if (setupInfo != null) + if (setupInfo != null && setupInfo.Container != null) { - if (!string.IsNullOrEmpty(setupInfo.Image)) + if (!string.IsNullOrEmpty(setupInfo.Container.Image)) { - if (!imagesToPull.ContainsKey(setupInfo.Image)) + if (!state.ImagesToPull.ContainsKey(setupInfo.Container.Image)) { - imagesToPull[setupInfo.Image] = new List(); + state.ImagesToPull[setupInfo.Container.Image] = new List(); } - Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to pull image '{setupInfo.Image}'"); - imagesToPull[setupInfo.Image].Add(action.Id); + Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.Container.ActionRepository}' needs to pull image '{setupInfo.Container.Image}'"); + state.ImagesToPull[setupInfo.Container.Image].Add(action.Id); } else { - ArgUtil.NotNullOrEmpty(setupInfo.ActionRepository, nameof(setupInfo.ActionRepository)); + ArgUtil.NotNullOrEmpty(setupInfo.Container.ActionRepository, nameof(setupInfo.Container.ActionRepository)); - if (!imagesToBuild.ContainsKey(setupInfo.ActionRepository)) + if (!state.ImagesToBuild.ContainsKey(setupInfo.Container.ActionRepository)) { - imagesToBuild[setupInfo.ActionRepository] = new List(); + state.ImagesToBuild[setupInfo.Container.ActionRepository] = new List(); } - Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to build image '{setupInfo.Dockerfile}'"); - imagesToBuild[setupInfo.ActionRepository].Add(action.Id); - imagesToBuildInfo[setupInfo.ActionRepository] = setupInfo; + Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.Container.ActionRepository}' needs to build image '{setupInfo.Container.Dockerfile}'"); + state.ImagesToBuild[setupInfo.Container.ActionRepository].Add(action.Id); + state.ImagesToBuildInfo[setupInfo.Container.ActionRepository] = setupInfo.Container; } } - + else if(setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0) + { + state = await PrepareActionsRecursiveAsync(executionContext, state, setupInfo.Steps, depth + 1); + } var repoAction = action.Reference as Pipelines.RepositoryPathReference; if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias) { var definition = LoadAction(executionContext, action); - if (definition.Data.Execution.HasPre) + // TODO: Support pre's in composite actions + if (definition.Data.Execution.HasPre && depth < 1) { var actionRunner = HostContext.CreateService(); actionRunner.Action = action; @@ -218,46 +204,13 @@ namespace GitHub.Runner.Worker actionRunner.Condition = definition.Data.Execution.InitCondition; Trace.Info($"Add 'pre' execution for {action.Id}"); - preStepTracker[action.Id] = actionRunner; + state.PreStepTracker[action.Id] = actionRunner; } } } } - if (imagesToPull.Count > 0) - { - foreach (var imageToPull in imagesToPull) - { - Trace.Info($"{imageToPull.Value.Count} steps need to pull image '{imageToPull.Key}'"); - containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.PullActionContainerAsync, - condition: $"{PipelineTemplateConstants.Success}()", - displayName: $"Pull {imageToPull.Key}", - data: new ContainerSetupInfo(imageToPull.Value, imageToPull.Key))); - } - } - - if (imagesToBuild.Count > 0) - { - foreach (var imageToBuild in imagesToBuild) - { - var setupInfo = imagesToBuildInfo[imageToBuild.Key]; - Trace.Info($"{imageToBuild.Value.Count} steps need to build image from '{setupInfo.Dockerfile}'"); - containerSetupSteps.Add(new JobExtensionRunner(runAsync: this.BuildActionContainerAsync, - condition: $"{PipelineTemplateConstants.Success}()", - displayName: $"Build {setupInfo.ActionRepository}", - data: new ContainerSetupInfo(imageToBuild.Value, setupInfo.Dockerfile, setupInfo.WorkingDirectory))); - } - } - -#if !OS_LINUX - if (containerSetupSteps.Count > 0) - { - executionContext.Output("Container action is only supported on Linux, skip pull and build docker images."); - containerSetupSteps.Clear(); - } -#endif - - return new PrepareResult(containerSetupSteps, preStepTracker); + return state; } public Definition LoadAction(IExecutionContext executionContext, Pipelines.ActionStep action) @@ -647,90 +600,6 @@ namespace GitHub.Runner.Worker return actionDownloadInfos.Actions; } - // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed - private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction) - { - Trace.Entering(); - ArgUtil.NotNull(executionContext, nameof(executionContext)); - - var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference; - ArgUtil.NotNull(repositoryReference, nameof(repositoryReference)); - - if (string.Equals(repositoryReference.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase)) - { - Trace.Info($"Repository action is in 'self' repository."); - return; - } - - if (!string.Equals(repositoryReference.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase)) - { - throw new NotSupportedException(repositoryReference.RepositoryType); - } - - ArgUtil.NotNullOrEmpty(repositoryReference.Name, nameof(repositoryReference.Name)); - ArgUtil.NotNullOrEmpty(repositoryReference.Ref, nameof(repositoryReference.Ref)); - - string destDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repositoryReference.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repositoryReference.Ref); - string watermarkFile = GetWatermarkFilePath(destDirectory); - if (File.Exists(watermarkFile)) - { - executionContext.Debug($"Action '{repositoryReference.Name}@{repositoryReference.Ref}' already downloaded at '{destDirectory}'."); - return; - } - else - { - // make sure we get a clean folder ready to use. - IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken); - Directory.CreateDirectory(destDirectory); - executionContext.Output($"Download action repository '{repositoryReference.Name}@{repositoryReference.Ref}'"); - } - - var configurationStore = HostContext.GetService(); - var isHostedServer = configurationStore.GetSettings().IsHostedServer; - if (isHostedServer) - { - string apiUrl = GetApiUrl(executionContext); - string archiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref); - var downloadDetails = new ActionDownloadDetails(archiveLink, ConfigureAuthorizationFromContext); - await DownloadRepositoryActionAsync(executionContext, downloadDetails, null, destDirectory); - return; - } - else - { - string apiUrl = GetApiUrl(executionContext); - - // URLs to try: - var downloadAttempts = new List { - // A built-in action or an action the user has created, on their GHES instance - // Example: https://my-ghes/api/v3/repos/my-org/my-action/tarball/v1 - new ActionDownloadDetails( - BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref), - ConfigureAuthorizationFromContext), - - // The same action, on GitHub.com - // Example: https://api.github.com/repos/my-org/my-action/tarball/v1 - new ActionDownloadDetails( - BuildLinkToActionArchive(_dotcomApiUrl, repositoryReference.Name, repositoryReference.Ref), - configureAuthorization: (e,h) => { /* no authorization for dotcom */ }) - }; - - foreach (var downloadAttempt in downloadAttempts) - { - try - { - await DownloadRepositoryActionAsync(executionContext, downloadAttempt, null, destDirectory); - return; - } - catch (ActionNotFoundException) - { - Trace.Info($"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}' at {downloadAttempt.ArchiveLink}"); - continue; - } - } - throw new ActionNotFoundException($"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}'. Paths attempted: {string.Join(", ", downloadAttempts.Select(d => d.ArchiveLink))}"); - } - } - private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, WebApi.ActionDownloadInfo downloadInfo) { Trace.Entering(); @@ -754,7 +623,7 @@ namespace GitHub.Runner.Worker executionContext.Output($"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'"); } - await DownloadRepositoryActionAsync(executionContext, null, downloadInfo, destDirectory); + await DownloadRepositoryActionAsync(executionContext, downloadInfo, destDirectory); } private string GetApiUrl(IExecutionContext executionContext) @@ -777,8 +646,7 @@ namespace GitHub.Runner.Worker #endif } - // todo: Remove the parameter "actionDownloadDetails" when feature flag DistributedTask.NewActionMetadata is removed - private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, ActionDownloadDetails actionDownloadDetails, WebApi.ActionDownloadInfo downloadInfo, string destDirectory) + private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, WebApi.ActionDownloadInfo downloadInfo, string destDirectory) { //download and extract action in a temp folder and rename it on success string tempDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), "_temp_" + Guid.NewGuid()); @@ -786,10 +654,10 @@ namespace GitHub.Runner.Worker #if OS_WINDOWS string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.zip"); - string link = downloadInfo?.ZipballUrl ?? actionDownloadDetails.ArchiveLink; + string link = downloadInfo?.ZipballUrl; #else string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.tar.gz"); - string link = downloadInfo?.TarballUrl ?? actionDownloadDetails.ArchiveLink; + string link = downloadInfo?.TarballUrl; #endif Trace.Info($"Save archive '{link}' into {archiveFile}."); @@ -811,16 +679,7 @@ namespace GitHub.Runner.Worker using (var httpClientHandler = HostContext.CreateHttpClientHandler()) using (var httpClient = new HttpClient(httpClientHandler)) { - // Legacy - if (downloadInfo == null) - { - actionDownloadDetails.ConfigureAuthorization(executionContext, httpClient); - } - // FF DistributedTask.NewActionMetadata - else - { - httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadInfo.Authentication?.Token); - } + httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadInfo.Authentication?.Token); httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); using (var response = await httpClient.GetAsync(link)) @@ -960,7 +819,6 @@ namespace GitHub.Runner.Worker } } - // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed private void ConfigureAuthorizationFromContext(IExecutionContext executionContext, HttpClient httpClient) { var authToken = Environment.GetEnvironmentVariable("_GITHUB_ACTION_TOKEN"); @@ -986,7 +844,7 @@ namespace GitHub.Runner.Worker private string GetWatermarkFilePath(string directory) => directory + ".completed"; - private ActionContainer PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction) + private ActionSetupInfo PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction) { var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference; if (string.Equals(repositoryReference.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase)) @@ -994,8 +852,8 @@ namespace GitHub.Runner.Worker Trace.Info($"Repository action is in 'self' repository."); return null; } - - var setupInfo = new ActionContainer(); + var setupInfo = new ActionSetupInfo(); + var actionContainer = new ActionContainer(); string destDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repositoryReference.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repositoryReference.Ref); string actionEntryDirectory = destDirectory; string dockerFileRelativePath = repositoryReference.Name; @@ -1004,11 +862,11 @@ namespace GitHub.Runner.Worker { actionEntryDirectory = Path.Combine(destDirectory, repositoryReference.Path); dockerFileRelativePath = $"{dockerFileRelativePath}/{repositoryReference.Path}"; - setupInfo.ActionRepository = $"{repositoryReference.Name}/{repositoryReference.Path}@{repositoryReference.Ref}"; + actionContainer.ActionRepository = $"{repositoryReference.Name}/{repositoryReference.Path}@{repositoryReference.Ref}"; } else { - setupInfo.ActionRepository = $"{repositoryReference.Name}@{repositoryReference.Ref}"; + actionContainer.ActionRepository = $"{repositoryReference.Name}@{repositoryReference.Ref}"; } // find the docker file or action.yml file @@ -1038,8 +896,9 @@ namespace GitHub.Runner.Worker var dockerFileFullPath = Path.Combine(actionEntryDirectory, containerAction.Image); executionContext.Debug($"Dockerfile for action: '{dockerFileFullPath}'."); - setupInfo.Dockerfile = dockerFileFullPath; - setupInfo.WorkingDirectory = destDirectory; + actionContainer.Dockerfile = dockerFileFullPath; + actionContainer.WorkingDirectory = destDirectory; + setupInfo.Container = actionContainer; return setupInfo; } else if (containerAction.Image.StartsWith("docker://", StringComparison.OrdinalIgnoreCase)) @@ -1048,7 +907,8 @@ namespace GitHub.Runner.Worker executionContext.Debug($"Container image for action: '{actionImage}'."); - setupInfo.Image = actionImage; + actionContainer.Image = actionImage; + setupInfo.Container = actionContainer; return setupInfo; } else @@ -1068,8 +928,21 @@ namespace GitHub.Runner.Worker } else if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Composite) { - Trace.Info($"Action composite: {(actionDefinitionData.Execution as CompositeActionExecutionData).Steps}, no more preparation."); - return null; + // TODO: we need to generate unique Id's for composite steps + Trace.Info($"Loading Composite steps"); + var compositeAction = actionDefinitionData.Execution as CompositeActionExecutionData; + setupInfo.Steps = compositeAction.Steps; + + foreach (var step in compositeAction.Steps) + { + step.Id = Guid.NewGuid(); + if (string.IsNullOrEmpty(executionContext.Global.Variables.Get("DistributedTask.EnableCompositeActions")) && step.Reference.Type != Pipelines.ActionSourceType.Script) + { + throw new Exception("`uses:` keyword is not currently supported."); + } + } + + return setupInfo; } else { @@ -1079,15 +952,17 @@ namespace GitHub.Runner.Worker else if (File.Exists(dockerFile)) { executionContext.Debug($"Dockerfile for action: '{dockerFile}'."); - setupInfo.Dockerfile = dockerFile; - setupInfo.WorkingDirectory = destDirectory; + actionContainer.Dockerfile = dockerFile; + actionContainer.WorkingDirectory = destDirectory; + setupInfo.Container = actionContainer; return setupInfo; } else if (File.Exists(dockerFileLowerCase)) { executionContext.Debug($"Dockerfile for action: '{dockerFileLowerCase}'."); - setupInfo.Dockerfile = dockerFileLowerCase; - setupInfo.WorkingDirectory = destDirectory; + actionContainer.Dockerfile = dockerFileLowerCase; + actionContainer.WorkingDirectory = destDirectory; + setupInfo.Container = actionContainer; return setupInfo; } else @@ -1140,20 +1015,6 @@ namespace GitHub.Runner.Worker HostContext.SecretMasker.AddValue(base64EncodingToken); return new AuthenticationHeaderValue("Basic", base64EncodingToken); } - - // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed - private class ActionDownloadDetails - { - public string ArchiveLink { get; } - - public Action ConfigureAuthorization { get; } - - public ActionDownloadDetails(string archiveLink, Action configureAuthorization) - { - ArchiveLink = archiveLink; - ConfigureAuthorization = configureAuthorization; - } - } } public sealed class Definition @@ -1303,4 +1164,18 @@ namespace GitHub.Runner.Worker public string WorkingDirectory { get; set; } public string ActionRepository { get; set; } } + + public class ActionSetupInfo + { + public ActionContainer Container { get; set; } + public List Steps {get; set;} + } + + public class PrepareActionsState + { + public Dictionary> ImagesToPull; + public Dictionary> ImagesToBuild; + public Dictionary ImagesToBuildInfo; + public Dictionary PreStepTracker; + } } diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 78226785c..f0890b8ee 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -245,6 +245,12 @@ namespace GitHub.Runner.Worker public void RegisterPostJobStep(IStep step) { + // TODO: Remove when we support composite post job steps + if (this.IsEmbedded) + { + throw new Exception("Composite actions do not currently support post steps"); + + } if (step is IActionRunner actionRunner && !Root.StepsWithPostRegistered.Add(actionRunner.Action.Id)) { Trace.Info($"'post' of '{actionRunner.DisplayName}' already push to post step stack."); diff --git a/src/Runner.Worker/Handlers/CompositeActionHandler.cs b/src/Runner.Worker/Handlers/CompositeActionHandler.cs index 0d1d2a480..53b2a02f5 100644 --- a/src/Runner.Worker/Handlers/CompositeActionHandler.cs +++ b/src/Runner.Worker/Handlers/CompositeActionHandler.cs @@ -5,11 +5,15 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using GitHub.DistributedTask.Expressions2; using GitHub.DistributedTask.ObjectTemplating.Tokens; using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.DistributedTask.Pipelines.ObjectTemplating; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common; using GitHub.Runner.Sdk; +using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Expressions; using Pipelines = GitHub.DistributedTask.Pipelines; @@ -142,6 +146,9 @@ namespace GitHub.Runner.Worker.Handlers { Trace.Info($"Processing embedded step: DisplayName='{step.DisplayName}'"); + // Add Expression Functions + step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo(PipelineTemplateConstants.HashFiles, 1, byte.MaxValue)); + // Initialize env context Trace.Info("Initialize Env context for embedded step"); #if OS_WINDOWS diff --git a/src/Runner.Worker/action_yaml.json b/src/Runner.Worker/action_yaml.json index 2873da7cd..bcfc98f15 100644 --- a/src/Runner.Worker/action_yaml.json +++ b/src/Runner.Worker/action_yaml.json @@ -112,7 +112,13 @@ "item-type": "composite-step" } }, - "composite-step": { + "composite-step":{ + "one-of": [ + "run-step", + "uses-step" + ] + }, + "run-step": { "mapping": { "properties": { "name": "string-steps-context", @@ -130,6 +136,20 @@ } } }, + "uses-step": { + "mapping": { + "properties": { + "name": "string-steps-context", + "id": "non-empty-string", + "uses": { + "type": "non-empty-string", + "required": true + }, + "with": "step-with", + "env": "step-env" + } + } + }, "container-runs-context": { "context": [ "inputs" @@ -195,6 +215,23 @@ "loose-key-type": "non-empty-string", "loose-value-type": "string" } + }, + "step-with": { + "context": [ + "github", + "inputs", + "strategy", + "matrix", + "steps", + "job", + "runner", + "env", + "hashFiles(1,255)" + ], + "mapping": { + "loose-key-type": "non-empty-string", + "loose-value-type": "string" + } } } } diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index 0a343ebc1..2f08b382c 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -34,144 +34,6 @@ namespace GitHub.Runner.Common.Tests.Worker private ActionManager _actionManager; private string _workFolder; -#if OS_LINUX - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_PullImageFromDockerHub_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.ContainerRegistryReference() - { - Image = "ubuntu:16.04" - } - } - }; - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - //Assert - Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal("ubuntu:16.04", (steps[0].Data as ContainerSetupInfo).Container.Image); - } - finally - { - Teardown(); - } - } -#endif - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_DownloadActionFromGraph_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "actions/download-artifact", - Ref = "master", - RepositoryType = "GitHub" - } - } - }; - - //Act - await _actionManager.PrepareActionsAsync(_ec.Object, actions); - - //Assert - var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions/download-artifact", "master.completed"); - Assert.True(File.Exists(watermarkFile)); - - var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions/download-artifact", "master", "action.yml"); - Assert.True(File.Exists(actionYamlFile)); - _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_DownloadBuiltInActionFromGraph_OnPremises_Legacy() - { - try - { - // Arrange - Setup(newActionMetadata: false); - const string ActionName = "actions/sample-action"; - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = Guid.NewGuid(), - Reference = new Pipelines.RepositoryPathReference() - { - Name = ActionName, - Ref = "main", - RepositoryType = "GitHub" - } - } - }; - - // Return a valid action from GHES via mock - const string ApiUrl = "https://ghes.example.com/api/v3"; - string expectedArchiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "main"); - string archiveFile = await CreateRepoArchive(); - using var stream = File.OpenRead(archiveFile); - var mockClientHandler = new Mock(); - mockClientHandler.Protected().Setup>("SendAsync", ItExpr.Is(m => m.RequestUri == new Uri(expectedArchiveLink)), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) }); - - var mockHandlerFactory = new Mock(); - mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny())).Returns(mockClientHandler.Object); - _hc.SetSingleton(mockHandlerFactory.Object); - - _ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl); - _configurationStore.Object.GetSettings().IsHostedServer = false; - - //Act - await _actionManager.PrepareActionsAsync(_ec.Object, actions); - - //Assert - var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main.completed"); - Assert.True(File.Exists(watermarkFile)); - - var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main", "action.yml"); - Assert.True(File.Exists(actionYamlFile)); - _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); - } - finally - { - Teardown(); - } - } - [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -180,7 +42,7 @@ namespace GitHub.Runner.Common.Tests.Worker try { // Arrange - Setup(newActionMetadata: false); + Setup(); const string ActionName = "ownerName/sample-action"; var actions = new List { @@ -241,7 +103,7 @@ namespace GitHub.Runner.Common.Tests.Worker try { // Arrange - Setup(newActionMetadata: false); + Setup(); const string ActionName = "ownerName/sample-action"; var actions = new List { @@ -292,1545 +154,6 @@ namespace GitHub.Runner.Common.Tests.Worker } } - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_AlwaysClearActionsCache_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List(); - - var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "notexist/no", "notexist.completed"); - Directory.CreateDirectory(Path.GetDirectoryName(watermarkFile)); - File.WriteAllText(watermarkFile, DateTime.UtcNow.ToString()); - Directory.CreateDirectory(Path.Combine(Path.GetDirectoryName(watermarkFile), "notexist")); - File.Copy(Path.Combine(TestUtil.GetSrcPath(), "Test", TestDataFolderName, "dockerfileaction.yml"), Path.Combine(Path.GetDirectoryName(watermarkFile), "notexist", "action.yml")); - - //Act - await _actionManager.PrepareActionsAsync(_ec.Object, actions); - - // Make sure _actions folder get deleted - Assert.False(Directory.Exists(_hc.GetDirectory(WellKnownDirectory.Actions))); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_SkipDownloadActionForSelfRepo_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Path = "action", - RepositoryType = Pipelines.PipelineConstants.SelfAlias - } - } - }; - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - Assert.True(steps.Count == 0); - } - finally - { - Teardown(); - } - } - -#if OS_LINUX - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithDockerfile_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithdockerfile", - RepositoryType = "GitHub" - } - } - }; - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfile"); - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithDockerfileInRelativePath_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithdockerfileinrelativepath", - Path = "images/cli", - RepositoryType = "GitHub" - } - } - }; - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfileinrelativepath"); - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "images/cli", "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithActionfile_Dockerfile_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithdockerfileinrelativepath", - RepositoryType = "GitHub" - } - } - }; - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfileinrelativepath"); - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithActionfile_DockerfileRelativePath_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "RepositoryActionWithActionfile_DockerfileRelativePath", - RepositoryType = "GitHub" - } - } - }; - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "RepositoryActionWithActionfile_DockerfileRelativePath"); - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "images/Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithActionfile_DockerHubImage_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "RepositoryActionWithActionfile_DockerHubImage", - RepositoryType = "GitHub" - } - } - }; - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "RepositoryActionWithActionfile_DockerHubImage"); - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal("ubuntu:18.04", (steps[0].Data as ContainerSetupInfo).Container.Image); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithActionYamlFile_DockerHubImage_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "RepositoryActionWithActionYamlFile_DockerHubImage", - RepositoryType = "GitHub" - } - } - }; - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "RepositoryActionWithActionYamlFile_DockerHubImage"); - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - Assert.Equal((steps[0].Data as ContainerSetupInfo).StepIds[0], actionId); - Assert.Equal("ubuntu:18.04", (steps[0].Data as ContainerSetupInfo).Container.Image); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithActionfileAndDockerfile_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithactionfileanddockerfile", - RepositoryType = "GitHub" - } - } - }; - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithactionfileanddockerfile"); - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_NotPullOrBuildImagesMultipleTimes_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId1 = Guid.NewGuid(); - var actionId2 = Guid.NewGuid(); - var actionId3 = Guid.NewGuid(); - var actionId4 = Guid.NewGuid(); - var actionId5 = Guid.NewGuid(); - var actionId6 = Guid.NewGuid(); - var actionId7 = Guid.NewGuid(); - var actionId8 = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId1, - Reference = new Pipelines.ContainerRegistryReference() - { - Image = "ubuntu:16.04" - } - }, - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId2, - Reference = new Pipelines.ContainerRegistryReference() - { - Image = "ubuntu:18.04" - } - }, - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId3, - Reference = new Pipelines.ContainerRegistryReference() - { - Image = "ubuntu:18.04" - } - }, - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId4, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "notpullorbuildimagesmultipletimes1", - RepositoryType = "GitHub" - } - }, - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId5, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithdockerfile", - RepositoryType = "GitHub" - } - }, - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId6, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithdockerfileinrelativepath", - RepositoryType = "GitHub" - } - }, - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId7, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithdockerfileinrelativepath", - RepositoryType = "GitHub" - } - }, - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId8, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "repositoryactionwithdockerfileinrelativepath", - Path = "images/cli", - RepositoryType = "GitHub" - } - } - }; - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - //Assert - Assert.Equal(actionId1, (steps[0].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal("ubuntu:16.04", (steps[0].Data as ContainerSetupInfo).Container.Image); - - Assert.Contains(actionId2, (steps[1].Data as ContainerSetupInfo).StepIds); - Assert.Contains(actionId3, (steps[1].Data as ContainerSetupInfo).StepIds); - Assert.Contains(actionId4, (steps[1].Data as ContainerSetupInfo).StepIds); - Assert.Equal("ubuntu:18.04", (steps[1].Data as ContainerSetupInfo).Container.Image); - - var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfile"); - - Assert.Equal(actionId5, (steps[2].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal(actionDir, (steps[2].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[2].Data as ContainerSetupInfo).Container.Dockerfile); - - actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfileinrelativepath"); - - Assert.Contains(actionId6, (steps[3].Data as ContainerSetupInfo).StepIds); - Assert.Contains(actionId7, (steps[3].Data as ContainerSetupInfo).StepIds); - Assert.Equal(actionDir, (steps[3].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[3].Data as ContainerSetupInfo).Container.Dockerfile); - - Assert.Equal(actionId8, (steps[4].Data as ContainerSetupInfo).StepIds[0]); - Assert.Equal(actionDir, (steps[4].Data as ContainerSetupInfo).Container.WorkingDirectory); - Assert.Equal(Path.Combine(actionDir, "images/cli", "Dockerfile"), (steps[4].Data as ContainerSetupInfo).Container.Dockerfile); - } - finally - { - Teardown(); - } - } -#endif - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithActionfile_Node_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "actions/setup-node", - Ref = "v1", - RepositoryType = "GitHub" - } - } - }; - - //Act - var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; - - // node.js based action doesn't need any extra steps to build/pull containers. - Assert.True(steps.Count == 0); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithInvalidWrapperActionfile_Node_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - var actionId = Guid.NewGuid(); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = actionId, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "RepositoryActionWithInvalidWrapperActionfile_Node", - RepositoryType = "GitHub" - } - } - }; - - //Act - try - { - await _actionManager.PrepareActionsAsync(_ec.Object, actions); - } - catch (ArgumentException) - { - var traceFile = Path.GetTempFileName(); - File.Copy(_hc.TraceFileName, traceFile, true); - Assert.Contains("You are using a JavaScript Action but there is not an entry JavaScript file provided in", File.ReadAllText(traceFile)); - } - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_RepositoryActionWithWrapperActionfile_PreSteps_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - - _hc.EnqueueInstance(new Mock().Object); - _hc.EnqueueInstance(new Mock().Object); - - var actionId1 = Guid.NewGuid(); - var actionId2 = Guid.NewGuid(); - _hc.GetTrace().Info(actionId1); - _hc.GetTrace().Info(actionId2); - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action1", - Id = actionId1, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "RepositoryActionWithWrapperActionfile_Node", - RepositoryType = "GitHub" - } - }, - new Pipelines.ActionStep() - { - Name = "action2", - Id = actionId2, - Reference = new Pipelines.RepositoryPathReference() - { - Name = "TingluoHuang/runner_L0", - Ref = "RepositoryActionWithWrapperActionfile_Docker", - RepositoryType = "GitHub" - } - } - }; - - //Act - var preResult = await _actionManager.PrepareActionsAsync(_ec.Object, actions); - Assert.Equal(2, preResult.PreStepTracker.Count); - Assert.NotNull(preResult.PreStepTracker[actionId1]); - Assert.NotNull(preResult.PreStepTracker[actionId2]); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsContainerRegistryActionDefinition_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - - Pipelines.ActionStep instance = new Pipelines.ActionStep() - { - Id = Guid.NewGuid(), - Reference = new Pipelines.ContainerRegistryReference() - { - Image = "ubuntu:16.04" - } - }; - - _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "ubuntu:16.04" }; - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.NotNull(definition.Data); - Assert.Equal("ubuntu:16.04", (definition.Data.Execution as ContainerActionExecutionData).Image); - Assert.True(string.IsNullOrEmpty((definition.Data.Execution as ContainerActionExecutionData).EntryPoint)); - Assert.Null((definition.Data.Execution as ContainerActionExecutionData).Arguments); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsScriptActionDefinition_Legacy() - { - try - { - //Arrange - Setup(newActionMetadata: false); - - Pipelines.ActionStep instance = new Pipelines.ActionStep() - { - Id = Guid.NewGuid(), - Reference = new Pipelines.ScriptReference() - }; - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.NotNull(definition.Data); - Assert.True(definition.Data.Execution.ExecutionType == ActionExecutionType.Script); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsContainerActionDefinitionDockerfile_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - // Prepare the task.json content. - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'docker' - image: 'Dockerfile' - args: - - '${{ inputs.greeting }}' - entrypoint: 'main.sh' - env: - Token: foo - Url: bar -"; - Pipelines.ActionStep instance; - string directory; - CreateAction(yamlContent: Content, instance: out instance, directory: out directory); - _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "image:1234" }; - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as ContainerActionExecutionData)); // execution.Node - Assert.Equal("image:1234", (definition.Data.Execution as ContainerActionExecutionData).Image); - Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); - - foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) - { - Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); - } - - foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) - { - var key = env.Key.AssertString("key").Value; - if (key == "Token") - { - Assert.Equal("foo", env.Value.AssertString("value").Value); - } - else if (key == "Url") - { - Assert.Equal("bar", env.Value.AssertString("value").Value); - } - else - { - throw new NotSupportedException(key); - } - } - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsContainerActionDefinitionRegistry_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - // Prepare the task.json content. - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'docker' - image: 'docker://ubuntu:16.04' - args: - - '${{ inputs.greeting }}' - entrypoint: 'main.sh' - env: - Token: foo - Url: ${{inputs.greeting}} -"; - Pipelines.ActionStep instance; - string directory; - CreateAction(yamlContent: Content, instance: out instance, directory: out directory); - - _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "ubuntu:16.04" }; - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as ContainerActionExecutionData)); - Assert.Equal("ubuntu:16.04", (definition.Data.Execution as ContainerActionExecutionData).Image); - Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); - - foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) - { - Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); - } - - foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) - { - var key = env.Key.AssertString("key").Value; - if (key == "Token") - { - Assert.Equal("foo", env.Value.AssertString("value").Value); - } - else if (key == "Url") - { - Assert.Equal("${{ inputs.greeting }}", env.Value.AssertScalar("value").ToString()); - } - else - { - throw new NotSupportedException(key); - } - } - - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsNodeActionDefinition_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'node12' - main: 'task.js' -"; - Pipelines.ActionStep instance; - string directory; - CreateAction(yamlContent: Content, instance: out instance, directory: out directory); - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as NodeJSActionExecutionData)); - Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsNodeActionDefinitionYaml_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'node12' - main: 'task.js' -"; - Pipelines.ActionStep instance; - string directory; - directory = Path.Combine(_workFolder, Constants.Path.ActionsDirectory, "GitHub/actions".Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), "main"); - string file = Path.Combine(directory, Constants.Path.ActionManifestYamlFile); - Directory.CreateDirectory(Path.GetDirectoryName(file)); - File.WriteAllText(file, Content); - instance = new Pipelines.ActionStep() - { - Id = Guid.NewGuid(), - Reference = new Pipelines.RepositoryPathReference() - { - Name = "GitHub/actions", - Ref = "main", - RepositoryType = Pipelines.RepositoryTypes.GitHub - } - }; - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as NodeJSActionExecutionData)); - Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsContainerActionDefinitionDockerfile_SelfRepo_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - // Prepare the task.json content. - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'docker' - image: 'Dockerfile' - args: - - '${{ inputs.greeting }}' - entrypoint: 'main.sh' - env: - Token: foo - Url: bar -"; - Pipelines.ActionStep instance; - string directory; - CreateSelfRepoAction(yamlContent: Content, instance: out instance, directory: out directory); - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as ContainerActionExecutionData)); // execution.Node - Assert.Equal("Dockerfile", (definition.Data.Execution as ContainerActionExecutionData).Image); - Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); - - foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) - { - Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); - } - - foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) - { - var key = env.Key.AssertString("key").Value; - if (key == "Token") - { - Assert.Equal("foo", env.Value.AssertString("value").Value); - } - else if (key == "Url") - { - Assert.Equal("bar", env.Value.AssertString("value").Value); - } - else - { - throw new NotSupportedException(key); - } - } - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsContainerActionDefinitionRegistry_SelfRepo_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - // Prepare the task.json content. - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'docker' - image: 'docker://ubuntu:16.04' - args: - - '${{ inputs.greeting }}' - entrypoint: 'main.sh' - env: - Token: foo - Url: ${{inputs.greeting}} -"; - Pipelines.ActionStep instance; - string directory; - CreateSelfRepoAction(yamlContent: Content, instance: out instance, directory: out directory); - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as ContainerActionExecutionData)); - Assert.Equal("docker://ubuntu:16.04", (definition.Data.Execution as ContainerActionExecutionData).Image); - Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); - - foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) - { - Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); - } - - foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) - { - var key = env.Key.AssertString("key").Value; - if (key == "Token") - { - Assert.Equal("foo", env.Value.AssertString("value").Value); - } - else if (key == "Url") - { - Assert.Equal("${{ inputs.greeting }}", env.Value.AssertScalar("value").ToString()); - } - else - { - throw new NotSupportedException(key); - } - } - - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsNodeActionDefinition_SelfRepo_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'node12' - main: 'task.js' -"; - Pipelines.ActionStep instance; - string directory; - CreateSelfRepoAction(yamlContent: Content, instance: out instance, directory: out directory); - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as NodeJSActionExecutionData)); - Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsNodeActionDefinition_Cleanup_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'node12' - main: 'task.js' - post: 'cleanup.js' -"; - Pipelines.ActionStep instance; - string directory; - CreateAction(yamlContent: Content, instance: out instance, directory: out directory); - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as NodeJSActionExecutionData)); - Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script); - Assert.Equal("cleanup.js", (definition.Data.Execution as NodeJSActionExecutionData).Post); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsContainerActionDefinitionDockerfile_Cleanup_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - // Prepare the task.json content. - const string Content = @" -# Container action -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'GitHub' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - using: 'docker' - image: 'Dockerfile' - args: - - '${{ inputs.greeting }}' - entrypoint: 'main.sh' - env: - Token: foo - Url: bar - post-entrypoint: 'cleanup.sh' -"; - Pipelines.ActionStep instance; - string directory; - CreateAction(yamlContent: Content, instance: out instance, directory: out directory); - _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "image:1234" }; - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as ContainerActionExecutionData)); // execution.Node - Assert.Equal("image:1234", (definition.Data.Execution as ContainerActionExecutionData).Image); - Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); - Assert.Equal("cleanup.sh", (definition.Data.Execution as ContainerActionExecutionData).Post); - - foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) - { - Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); - } - - foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) - { - var key = env.Key.AssertString("key").Value; - if (key == "Token") - { - Assert.Equal("foo", env.Value.AssertString("value").Value); - } - else if (key == "Url") - { - Assert.Equal("bar", env.Value.AssertString("value").Value); - } - else - { - throw new NotSupportedException(key); - } - } - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void LoadsPluginActionDefinition_Legacy() - { - try - { - // Arrange. - Setup(newActionMetadata: false); - const string Content = @" -name: 'Hello World' -description: 'Greet the world and record the time' -author: 'Test Corporation' -inputs: - greeting: # id of input - description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' - required: true - default: 'Hello' - entryPoint: # id of input - description: 'optional docker entrypoint overwrite.' - required: false -outputs: - time: # id of output - description: 'The time we did the greeting' -icon: 'hello.svg' # vector art to display in the GitHub Marketplace -color: 'green' # optional, decorates the entry in the GitHub Marketplace -runs: - plugin: 'someplugin' -"; - Pipelines.ActionStep instance; - string directory; - CreateAction(yamlContent: Content, instance: out instance, directory: out directory); - - // Act. - Definition definition = _actionManager.LoadAction(_ec.Object, instance); - - // Assert. - Assert.NotNull(definition); - Assert.Equal(directory, definition.Directory); - Assert.NotNull(definition.Data); - Assert.NotNull(definition.Data.Inputs); // inputs - Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var input in definition.Data.Inputs) - { - var name = input.Key.AssertString("key").Value; - var value = input.Value.AssertScalar("value").ToString(); - - _hc.GetTrace().Info($"Default: {name} = {value}"); - inputDefaults[name] = value; - } - - Assert.Equal(2, inputDefaults.Count); - Assert.True(inputDefaults.ContainsKey("greeting")); - Assert.Equal("Hello", inputDefaults["greeting"]); - Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); - Assert.NotNull(definition.Data.Execution); // execution - - Assert.NotNull((definition.Data.Execution as PluginActionExecutionData)); - Assert.Equal("plugin.class, plugin", (definition.Data.Execution as PluginActionExecutionData).Plugin); - Assert.Equal("plugin.cleanup, plugin", (definition.Data.Execution as PluginActionExecutionData).Post); - } - finally - { - Teardown(); - } - } - #if OS_LINUX [Fact] [Trait("Level", "L0")] @@ -2530,6 +853,171 @@ runs: } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_CompositeActionWithActionfile_Node() + { + try + { + //Arrange + Setup(); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositeBasic", + RepositoryType = "GitHub" + } + } + }; + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + // node.js based action doesn't need any extra steps to build/pull containers. + Assert.True(steps.Count == 0); + var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "CompositeBasic.completed"); + Assert.True(File.Exists(watermarkFile)); + // Comes from the composite action + var watermarkFile2 = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions/setup-node", "v2", "action.yml"); + Assert.True(File.Exists(watermarkFile2)); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_CompositeActionWithActionfile_MaxLimit() + { + try + { + //Arrange + Setup(); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositeLimit", + RepositoryType = "GitHub" + } + } + }; + + //Act + Func result = async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + var exception = await Assert.ThrowsAsync(result); + Assert.Equal($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}", exception.Message); + + // node.js based action doesn't need any extra steps to build/pull containers. + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_CompositeActionWithActionfile_CompositePrestepNested() + { + try + { + //Arrange + Setup(); + var actionId = Guid.NewGuid(); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositePrestep", + RepositoryType = "GitHub" + } + } + }; + + //Act + var result = await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + // TODO: Update this test + Assert.Equal(0, result.PreStepTracker.Count); + + } + finally + { + Teardown(); + } + } + +#if OS_LINUX + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_CompositeActionWithActionfile_CompositeContainerNested() + { + try + { + //Arrange + Setup(); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositeContainerNested", + RepositoryType = "GitHub" + } + } + }; + + //Act + var result = await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + Assert.Equal(2, result.ContainerSetupSteps.Count); + + // node.js based action doesn't need any extra steps to build/pull containers. + } + finally + { + Teardown(); + } + } +#endif + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -3563,7 +2051,7 @@ runs: #endif } - private void Setup([CallerMemberName] string name = "", bool newActionMetadata = true) + private void Setup([CallerMemberName] string name = "", bool enableComposite = true) { _ecTokenSource?.Dispose(); _ecTokenSource = new CancellationTokenSource(); @@ -3578,9 +2066,9 @@ runs: _ec.Setup(x => x.Global).Returns(new GlobalContext()); _ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token); var variables = new Dictionary(); - if (newActionMetadata) + if (enableComposite) { - variables["DistributedTask.NewActionMetadata"] = "true"; + variables["DistributedTask.EnableCompositeActions"] = "true"; } _ec.Object.Global.Variables = new Variables(_hc, variables); _ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());