private ActionStep(ActionStep actionToClone) : base(actionToClone) { this.Reference = actionToClone.Reference?.Clone(); Environment = actionToClone.Environment?.Clone(); Inputs = actionToClone.Inputs?.Clone(); ContextName = actionToClone?.ContextName; DisplayNameToken = actionToClone.DisplayNameToken?.Clone(); }
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 stepTypeValue)) { StepType stepType; if (stepTypeValue.Type == JTokenType.Integer) { stepType = (StepType)(Int32)stepTypeValue; } else if (stepTypeValue.Type != JTokenType.String || !Enum.TryParse((String)stepTypeValue, true, out stepType)) { return(null); } Step stepObject = null; switch (stepType) { case StepType.Action: stepObject = new ActionStep(); break; } using (var objectReader = value.CreateReader()) { serializer.Populate(objectReader, stepObject); } return(stepObject); } else { return(null); } }
private ActionContainer PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction) { var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference; if (string.Equals(repositoryReference.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase)) { Trace.Info($"Repository action is in 'self' repository."); return(null); } var setupInfo = 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; ArgUtil.NotNull(repositoryReference, nameof(repositoryReference)); if (!string.IsNullOrEmpty(repositoryReference.Path)) { actionEntryDirectory = Path.Combine(destDirectory, repositoryReference.Path); dockerFileRelativePath = $"{dockerFileRelativePath}/{repositoryReference.Path}"; setupInfo.ActionRepository = $"{repositoryReference.Name}/{repositoryReference.Path}@{repositoryReference.Ref}"; } else { setupInfo.ActionRepository = $"{repositoryReference.Name}@{repositoryReference.Ref}"; } // find the docker file or action.yml file var dockerFile = Path.Combine(actionEntryDirectory, "Dockerfile"); var dockerFileLowerCase = Path.Combine(actionEntryDirectory, "dockerfile"); var actionManifest = Path.Combine(actionEntryDirectory, Constants.Path.ActionManifestYmlFile); var actionManifestYaml = Path.Combine(actionEntryDirectory, Constants.Path.ActionManifestYamlFile); if (File.Exists(actionManifest) || File.Exists(actionManifestYaml)) { executionContext.Debug($"action.yml for action: '{actionManifest}'."); var manifestManager = HostContext.GetService <IActionManifestManager>(); ActionDefinitionData actionDefinitionData = null; if (File.Exists(actionManifest)) { actionDefinitionData = manifestManager.Load(executionContext, actionManifest); } else { actionDefinitionData = manifestManager.Load(executionContext, actionManifestYaml); } if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Container) { var containerAction = actionDefinitionData.Execution as ContainerActionExecutionData; if (containerAction.Image.EndsWith("Dockerfile") || containerAction.Image.EndsWith("dockerfile")) { var dockerFileFullPath = Path.Combine(actionEntryDirectory, containerAction.Image); executionContext.Debug($"Dockerfile for action: '{dockerFileFullPath}'."); setupInfo.Dockerfile = dockerFileFullPath; setupInfo.WorkingDirectory = destDirectory; return(setupInfo); } else if (containerAction.Image.StartsWith("docker://", StringComparison.OrdinalIgnoreCase)) { var actionImage = containerAction.Image.Substring("docker://".Length); executionContext.Debug($"Container image for action: '{actionImage}'."); setupInfo.Image = actionImage; return(setupInfo); } else { throw new NotSupportedException($"'{containerAction.Image}' should be either '[path]/Dockerfile' or 'docker://image[:tag]'."); } } else if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.NodeJS) { Trace.Info($"Action node.js file: {(actionDefinitionData.Execution as NodeJSActionExecutionData).Script}, no more preparation."); return(null); } else if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Plugin) { Trace.Info($"Action plugin: {(actionDefinitionData.Execution as PluginActionExecutionData).Plugin}, no more preparation."); return(null); } else { throw new NotSupportedException(actionDefinitionData.Execution.ExecutionType.ToString()); } } else if (File.Exists(dockerFile)) { executionContext.Debug($"Dockerfile for action: '{dockerFile}'."); setupInfo.Dockerfile = dockerFile; setupInfo.WorkingDirectory = destDirectory; return(setupInfo); } else if (File.Exists(dockerFileLowerCase)) { executionContext.Debug($"Dockerfile for action: '{dockerFileLowerCase}'."); setupInfo.Dockerfile = dockerFileLowerCase; setupInfo.WorkingDirectory = destDirectory; return(setupInfo); } else { var fullPath = IOUtil.ResolvePath(actionEntryDirectory, "."); // resolve full path without access filesystem. throw new InvalidOperationException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?"); } }
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 <IConfigurationStore>(); var isHostedServer = configurationStore.GetSettings().IsHostedServer; if (isHostedServer) { string apiUrl = GetApiUrl(executionContext); string archiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref); Trace.Info($"Download archive '{archiveLink}' to '{destDirectory}'."); var downloadDetails = new ActionDownloadDetails(archiveLink, ConfigureAuthorizationFromContext); await DownloadRepositoryActionAsync(executionContext, downloadDetails, destDirectory); return; } else { string apiUrl = GetApiUrl(executionContext); // URLs to try: var downloadAttempts = new List <ActionDownloadDetails> { // 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) { Trace.Info($"Download archive '{downloadAttempt.ArchiveLink}' to '{destDirectory}'."); try { await DownloadRepositoryActionAsync(executionContext, downloadAttempt, 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))}"); } }
public Definition LoadAction(IExecutionContext executionContext, Pipelines.ActionStep action) { // Validate args. Trace.Entering(); ArgUtil.NotNull(action, nameof(action)); // Initialize the definition wrapper object. var definition = new Definition() { Data = new ActionDefinitionData() }; if (action.Reference.Type == Pipelines.ActionSourceType.ContainerRegistry) { Trace.Info("Load action that reference container from registry."); CachedActionContainers.TryGetValue(action.Id, out var container); ArgUtil.NotNull(container, nameof(container)); definition.Data.Execution = new ContainerActionExecutionData() { Image = container.ContainerImage }; Trace.Info($"Using action container image: {container.ContainerImage}."); } else if (action.Reference.Type == Pipelines.ActionSourceType.Repository) { string actionDirectory = null; var repoAction = action.Reference as Pipelines.RepositoryPathReference; if (string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase)) { actionDirectory = executionContext.GetGitHubContext("workspace"); if (!string.IsNullOrEmpty(repoAction.Path)) { actionDirectory = Path.Combine(actionDirectory, repoAction.Path); } } else { actionDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repoAction.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repoAction.Ref); if (!string.IsNullOrEmpty(repoAction.Path)) { actionDirectory = Path.Combine(actionDirectory, repoAction.Path); } } Trace.Info($"Load action that reference repository from '{actionDirectory}'"); definition.Directory = actionDirectory; string manifestFile = Path.Combine(actionDirectory, Constants.Path.ActionManifestYmlFile); string manifestFileYaml = Path.Combine(actionDirectory, Constants.Path.ActionManifestYamlFile); string dockerFile = Path.Combine(actionDirectory, "Dockerfile"); string dockerFileLowerCase = Path.Combine(actionDirectory, "dockerfile"); if (File.Exists(manifestFile) || File.Exists(manifestFileYaml)) { var manifestManager = HostContext.GetService <IActionManifestManager>(); if (File.Exists(manifestFile)) { definition.Data = manifestManager.Load(executionContext, manifestFile); } else { definition.Data = manifestManager.Load(executionContext, manifestFileYaml); } Trace.Verbose($"Action friendly name: '{definition.Data.Name}'"); Trace.Verbose($"Action description: '{definition.Data.Description}'"); if (definition.Data.Inputs != null) { foreach (var input in definition.Data.Inputs) { Trace.Verbose($"Action input: '{input.Key.ToString()}' default to '{input.Value.ToString()}'"); } } if (definition.Data.Execution.ExecutionType == ActionExecutionType.Container) { var containerAction = definition.Data.Execution as ContainerActionExecutionData; Trace.Info($"Action container Dockerfile/image: {containerAction.Image}."); if (containerAction.Arguments != null) { Trace.Info($"Action container args: {StringUtil.ConvertToJson(containerAction.Arguments)}."); } if (containerAction.Environment != null) { Trace.Info($"Action container env: {StringUtil.ConvertToJson(containerAction.Environment)}."); } if (!string.IsNullOrEmpty(containerAction.Pre)) { Trace.Info($"Action container pre entrypoint: {containerAction.Pre}."); } if (!string.IsNullOrEmpty(containerAction.EntryPoint)) { Trace.Info($"Action container entrypoint: {containerAction.EntryPoint}."); } if (!string.IsNullOrEmpty(containerAction.Post)) { Trace.Info($"Action container post entrypoint: {containerAction.Post}."); } if (CachedActionContainers.TryGetValue(action.Id, out var container)) { Trace.Info($"Image '{containerAction.Image}' already built/pulled, use image: {container.ContainerImage}."); containerAction.Image = container.ContainerImage; } } else if (definition.Data.Execution.ExecutionType == ActionExecutionType.NodeJS) { var nodeAction = definition.Data.Execution as NodeJSActionExecutionData; Trace.Info($"Action pre node.js file: {nodeAction.Pre ?? "N/A"}."); Trace.Info($"Action node.js file: {nodeAction.Script}."); Trace.Info($"Action post node.js file: {nodeAction.Post ?? "N/A"}."); } else if (definition.Data.Execution.ExecutionType == ActionExecutionType.Plugin) { var pluginAction = definition.Data.Execution as PluginActionExecutionData; var pluginManager = HostContext.GetService <IRunnerPluginManager>(); var plugin = pluginManager.GetPluginAction(pluginAction.Plugin); ArgUtil.NotNull(plugin, pluginAction.Plugin); ArgUtil.NotNullOrEmpty(plugin.PluginTypeName, pluginAction.Plugin); pluginAction.Plugin = plugin.PluginTypeName; Trace.Info($"Action plugin: {plugin.PluginTypeName}."); if (!string.IsNullOrEmpty(plugin.PostPluginTypeName)) { pluginAction.Post = plugin.PostPluginTypeName; Trace.Info($"Action cleanup plugin: {plugin.PluginTypeName}."); } } else { throw new NotSupportedException(definition.Data.Execution.ExecutionType.ToString()); } } else if (File.Exists(dockerFile)) { if (CachedActionContainers.TryGetValue(action.Id, out var container)) { definition.Data.Execution = new ContainerActionExecutionData() { Image = container.ContainerImage }; } else { definition.Data.Execution = new ContainerActionExecutionData() { Image = dockerFile }; } } else if (File.Exists(dockerFileLowerCase)) { if (CachedActionContainers.TryGetValue(action.Id, out var container)) { definition.Data.Execution = new ContainerActionExecutionData() { Image = container.ContainerImage }; } else { definition.Data.Execution = new ContainerActionExecutionData() { Image = dockerFileLowerCase }; } } else { var fullPath = IOUtil.ResolvePath(actionDirectory, "."); // resolve full path without access filesystem. throw new NotSupportedException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?"); } } else if (action.Reference.Type == Pipelines.ActionSourceType.Script) { definition.Data.Execution = new ScriptActionExecutionData(); definition.Data.Name = "Run"; definition.Data.Description = "Execute a script"; } else { throw new NotSupportedException(action.Reference.Type.ToString()); } return(definition); }
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); if (File.Exists(destDirectory + ".completed")) { 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}'"); } #if OS_WINDOWS string archiveLink = $"https://api.github.com/repos/{repositoryReference.Name}/zipball/{repositoryReference.Ref}"; #else string archiveLink = $"https://api.github.com/repos/{repositoryReference.Name}/tarball/{repositoryReference.Ref}"; #endif Trace.Info($"Download archive '{archiveLink}' to '{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()); Directory.CreateDirectory(tempDirectory); #if OS_WINDOWS string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.zip"); #else string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.tar.gz"); #endif Trace.Info($"Save archive '{archiveLink}' into {archiveFile}."); try { int retryCount = 0; // Allow up to 20 * 60s for any action to be downloaded from github graph. int timeoutSeconds = 20 * 60; while (retryCount < 3) { using (var actionDownloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds))) using (var actionDownloadCancellation = CancellationTokenSource.CreateLinkedTokenSource(actionDownloadTimeout.Token, executionContext.CancellationToken)) { try { //open zip stream in async mode using (FileStream fs = new FileStream(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true)) using (var httpClientHandler = HostContext.CreateHttpClientHandler()) using (var httpClient = new HttpClient(httpClientHandler)) { var authToken = Environment.GetEnvironmentVariable("_GITHUB_ACTION_TOKEN"); if (string.IsNullOrEmpty(authToken)) { // TODO: Depreciate the PREVIEW_ACTION_TOKEN authToken = executionContext.Variables.Get("PREVIEW_ACTION_TOKEN"); } if (!string.IsNullOrEmpty(authToken)) { HostContext.SecretMasker.AddValue(authToken); var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"PAT:{authToken}")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodingToken); } else { var accessToken = executionContext.GetGitHubContext("token"); var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{accessToken}")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodingToken); } httpClient.DefaultRequestHeaders.UserAgent.Add(HostContext.UserAgent); using (var result = await httpClient.GetStreamAsync(archiveLink)) { await result.CopyToAsync(fs, _defaultCopyBufferSize, actionDownloadCancellation.Token); await fs.FlushAsync(actionDownloadCancellation.Token); // download succeed, break out the retry loop. break; } } } catch (OperationCanceledException) when(executionContext.CancellationToken.IsCancellationRequested) { Trace.Info($"Action download has been cancelled."); throw; } catch (Exception ex) when(retryCount < 2) { retryCount++; Trace.Error($"Fail to download archive '{archiveLink}' -- Attempt: {retryCount}"); Trace.Error(ex); if (actionDownloadTimeout.Token.IsCancellationRequested) { // action download didn't finish within timeout executionContext.Warning($"Action '{archiveLink}' didn't finish download within {timeoutSeconds} seconds."); } else { executionContext.Warning($"Failed to download action '{archiveLink}'. Error {ex.Message}"); } } } if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF"))) { var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)); executionContext.Warning($"Back off {backOff.TotalSeconds} seconds before retry."); await Task.Delay(backOff); } } ArgUtil.NotNullOrEmpty(archiveFile, nameof(archiveFile)); executionContext.Debug($"Download '{archiveLink}' to '{archiveFile}'"); var stagingDirectory = Path.Combine(tempDirectory, "_staging"); Directory.CreateDirectory(stagingDirectory); #if OS_WINDOWS ZipFile.ExtractToDirectory(archiveFile, stagingDirectory); #else string tar = WhichUtil.Which("tar", require: true, trace: Trace); // tar -xzf using (var processInvoker = HostContext.CreateService <IProcessInvoker>()) { processInvoker.OutputDataReceived += new EventHandler <ProcessDataReceivedEventArgs>((sender, args) => { if (!string.IsNullOrEmpty(args.Data)) { Trace.Info(args.Data); } }); processInvoker.ErrorDataReceived += new EventHandler <ProcessDataReceivedEventArgs>((sender, args) => { if (!string.IsNullOrEmpty(args.Data)) { Trace.Error(args.Data); } }); int exitCode = await processInvoker.ExecuteAsync(stagingDirectory, tar, $"-xzf \"{archiveFile}\"", null, executionContext.CancellationToken); if (exitCode != 0) { throw new NotSupportedException($"Can't use 'tar -xzf' extract archive file: {archiveFile}. return code: {exitCode}."); } } #endif // repository archive from github always contains a nested folder var subDirectories = new DirectoryInfo(stagingDirectory).GetDirectories(); if (subDirectories.Length != 1) { throw new InvalidOperationException($"'{archiveFile}' contains '{subDirectories.Length}' directories"); } else { executionContext.Debug($"Unwrap '{subDirectories[0].Name}' to '{destDirectory}'"); IOUtil.CopyDirectory(subDirectories[0].FullName, destDirectory, executionContext.CancellationToken); } Trace.Verbose("Create watermark file indicate action download succeed."); File.WriteAllText(destDirectory + ".completed", DateTime.UtcNow.ToString()); executionContext.Debug($"Archive '{archiveFile}' has been unzipped into '{destDirectory}'."); Trace.Info("Finished getting action repository."); } finally { try { //if the temp folder wasn't moved -> wipe it if (Directory.Exists(tempDirectory)) { Trace.Verbose("Deleting action temp folder: {0}", tempDirectory); IOUtil.DeleteDirectory(tempDirectory, CancellationToken.None); // Don't cancel this cleanup and should be pretty fast. } } catch (Exception ex) { //it is not critical if we fail to delete the temp folder Trace.Warning("Failed to delete temp folder '{0}'. Exception: {1}", tempDirectory, ex); } } }
public Task RunAsync(ActionRunStage stage) { // Validate args. Trace.Entering(); ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext)); ArgUtil.NotNull(Inputs, nameof(Inputs)); var githubContext = ExecutionContext.ExpressionValues["github"] as GitHubContext; ArgUtil.NotNull(githubContext, nameof(githubContext)); var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp); // Resolve action steps var actionSteps = Data.Steps; // Create Context Data to reuse for each composite action step var inputsData = new DictionaryContextData(); foreach (var i in Inputs) { inputsData[i.Key] = new StringContextData(i.Value); } // Add each composite action step to the front of the queue int location = 0; foreach (Pipelines.ActionStep aStep in actionSteps) { // Ex: // runs: // using: "composite" // steps: // - uses: example/test-composite@v2 (a) // - run echo hello world (b) // - run echo hello world 2 (c) // // ethanchewy/test-composite/action.yaml // runs: // using: "composite" // steps: // - run echo hello world 3 (d) // - run echo hello world 4 (e) // // Steps processed as follow: // | a | // | a | => | d | // (Run step d) // | a | // | a | => | e | // (Run step e) // | a | // (Run step a) // | b | // (Run step b) // | c | // (Run step c) // Done. var actionRunner = HostContext.CreateService <IActionRunner>(); actionRunner.Action = aStep; actionRunner.Stage = stage; actionRunner.Condition = aStep.Condition; var step = ExecutionContext.RegisterNestedStep(actionRunner, inputsData, location, Environment); InitializeScope(step); location++; } // Create a step that handles all the composite action steps' outputs Pipelines.ActionStep cleanOutputsStep = new Pipelines.ActionStep(); cleanOutputsStep.ContextName = ExecutionContext.ContextName; // Use the same reference type as our composite steps. cleanOutputsStep.Reference = Action; var actionRunner2 = HostContext.CreateService <IActionRunner>(); actionRunner2.Action = cleanOutputsStep; actionRunner2.Stage = ActionRunStage.Main; actionRunner2.Condition = "always()"; ExecutionContext.RegisterNestedStep(actionRunner2, inputsData, location, Environment, true); return(Task.CompletedTask); }