2023-06-02 21:47:59 +02:00
using System ;
2019-10-10 00:52:42 -04:00
using System.Collections.Generic ;
using System.IO ;
using System.IO.Compression ;
using System.Linq ;
2020-04-23 17:02:13 -04:00
using System.Net ;
2019-10-10 00:52:42 -04:00
using System.Net.Http ;
using System.Net.Http.Headers ;
2023-10-19 08:15:06 -04:00
using System.Security.Cryptography ;
2019-10-10 00:52:42 -04:00
using System.Text ;
2020-04-23 17:02:13 -04:00
using System.Threading ;
using System.Threading.Tasks ;
2019-10-10 00:52:42 -04:00
using GitHub.DistributedTask.ObjectTemplating.Tokens ;
2023-08-15 19:00:54 -04:00
using GitHub.DistributedTask.WebApi ;
2019-10-10 00:52:42 -04:00
using GitHub.Runner.Common ;
2023-05-03 16:04:21 -04:00
using GitHub.Runner.Common.Util ;
2019-10-10 00:52:42 -04:00
using GitHub.Runner.Sdk ;
using GitHub.Runner.Worker.Container ;
using GitHub.Services.Common ;
using Pipelines = GitHub . DistributedTask . Pipelines ;
using PipelineTemplateConstants = GitHub . DistributedTask . Pipelines . ObjectTemplating . PipelineTemplateConstants ;
2023-08-15 19:00:54 -04:00
using WebApi = GitHub . DistributedTask . WebApi ;
2019-10-10 00:52:42 -04:00
namespace GitHub.Runner.Worker
{
2020-04-13 21:46:30 -04:00
public class PrepareResult
{
public PrepareResult ( List < JobExtensionRunner > containerSetupSteps , Dictionary < Guid , IActionRunner > preStepTracker )
{
this . ContainerSetupSteps = containerSetupSteps ;
this . PreStepTracker = preStepTracker ;
}
public List < JobExtensionRunner > ContainerSetupSteps { get ; set ; }
public Dictionary < Guid , IActionRunner > PreStepTracker { get ; set ; }
}
2019-10-10 00:52:42 -04:00
[ServiceLocator(Default = typeof(ActionManager))]
public interface IActionManager : IRunnerService
{
Dictionary < Guid , ContainerInfo > CachedActionContainers { get ; }
2021-07-28 15:35:21 -04:00
Dictionary < Guid , List < Pipelines . ActionStep >> CachedEmbeddedPreSteps { get ; }
Dictionary < Guid , List < Guid >> CachedEmbeddedStepIds { get ; }
Dictionary < Guid , Stack < Pipelines . ActionStep >> CachedEmbeddedPostSteps { get ; }
Task < PrepareResult > PrepareActionsAsync ( IExecutionContext executionContext , IEnumerable < Pipelines . JobStep > steps , Guid rootStepId = default ( Guid ));
2019-10-10 00:52:42 -04:00
Definition LoadAction ( IExecutionContext executionContext , Pipelines . ActionStep action );
}
public sealed class ActionManager : RunnerService , IActionManager
{
private const int _defaultFileStreamBufferSize = 4096 ;
2020-01-20 18:22:59 +01:00
//81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k).
2019-10-10 00:52:42 -04:00
private const int _defaultCopyBufferSize = 81920 ;
2022-10-18 10:54:08 -04:00
private readonly Dictionary < Guid , ContainerInfo > _cachedActionContainers = new ();
2019-10-10 00:52:42 -04:00
public Dictionary < Guid , ContainerInfo > CachedActionContainers => _cachedActionContainers ;
2021-07-28 15:35:21 -04:00
2022-10-18 10:54:08 -04:00
private readonly Dictionary < Guid , List < Pipelines . ActionStep >> _cachedEmbeddedPreSteps = new ();
2021-07-28 15:35:21 -04:00
public Dictionary < Guid , List < Pipelines . ActionStep >> CachedEmbeddedPreSteps => _cachedEmbeddedPreSteps ;
2022-10-18 10:54:08 -04:00
private readonly Dictionary < Guid , List < Guid >> _cachedEmbeddedStepIds = new ();
2021-07-28 15:35:21 -04:00
public Dictionary < Guid , List < Guid >> CachedEmbeddedStepIds => _cachedEmbeddedStepIds ;
2022-10-18 10:54:08 -04:00
private readonly Dictionary < Guid , Stack < Pipelines . ActionStep >> _cachedEmbeddedPostSteps = new ();
2021-07-28 15:35:21 -04:00
public Dictionary < Guid , Stack < Pipelines . ActionStep >> CachedEmbeddedPostSteps => _cachedEmbeddedPostSteps ;
public async Task < PrepareResult > PrepareActionsAsync ( IExecutionContext executionContext , IEnumerable < Pipelines . JobStep > steps , Guid rootStepId = default ( Guid ))
2019-10-10 00:52:42 -04:00
{
2021-07-01 13:34:28 -04:00
// Assert inputs
2019-10-10 00:52:42 -04:00
ArgUtil . NotNull ( executionContext , nameof ( executionContext ));
ArgUtil . NotNull ( steps , nameof ( steps ));
2021-07-01 13:34:28 -04:00
var state = new PrepareActionsState
2019-10-10 00:52:42 -04:00
{
2021-07-01 13:34:28 -04:00
ImagesToBuild = new Dictionary < string , List < Guid >>( StringComparer . OrdinalIgnoreCase ),
ImagesToPull = new Dictionary < string , List < Guid >>( StringComparer . OrdinalIgnoreCase ),
ImagesToBuildInfo = new Dictionary < string , ActionContainer >( StringComparer . OrdinalIgnoreCase ),
PreStepTracker = new Dictionary < Guid , IActionRunner >()
};
var containerSetupSteps = new List < JobExtensionRunner >();
2021-07-28 15:35:21 -04:00
var depth = 0 ;
// We are running at the start of a job
if ( rootStepId == default ( Guid ))
{
IOUtil . DeleteDirectory ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), executionContext . CancellationToken );
}
// We are running mid job due to a local composite action
else
{
if (! _cachedEmbeddedStepIds . ContainsKey ( rootStepId ))
{
_cachedEmbeddedStepIds [ rootStepId ] = new List < Guid >();
foreach ( var compositeStep in steps )
{
var guid = Guid . NewGuid ();
compositeStep . Id = guid ;
_cachedEmbeddedStepIds [ rootStepId ]. Add ( guid );
}
}
depth = 1 ;
}
2021-07-01 13:34:28 -04:00
IEnumerable < Pipelines . ActionStep > actions = steps . OfType < Pipelines . ActionStep >();
executionContext . Output ( "Prepare all required actions" );
2023-05-11 14:57:24 -04:00
PrepareActionsState result = new PrepareActionsState ();
try
{
result = await PrepareActionsRecursiveAsync ( executionContext , state , actions , depth , rootStepId );
}
catch ( FailedToResolveActionDownloadInfoException ex )
{
// Log the error and fail the PrepareActionsAsync Initialization.
Trace . Error ( $"Caught exception from PrepareActionsAsync Initialization: {ex}" );
executionContext . InfrastructureError ( ex . Message );
executionContext . Result = TaskResult . Failed ;
throw ;
}
2023-09-06 09:32:57 -04:00
catch ( InvalidActionArchiveException ex )
{
// Log the error and fail the PrepareActionsAsync Initialization.
Trace . Error ( $"Caught exception from PrepareActionsAsync Initialization: {ex}" );
executionContext . InfrastructureError ( ex . Message );
executionContext . Result = TaskResult . Failed ;
throw ;
}
2022-06-10 15:51:20 +02:00
if (! FeatureManager . IsContainerHooksEnabled ( executionContext . Global . Variables ))
2021-07-01 13:34:28 -04:00
{
2022-06-10 15:51:20 +02:00
if ( state . ImagesToPull . Count > 0 )
2021-07-01 13:34:28 -04:00
{
2022-06-10 15:51:20 +02:00
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 )));
}
2021-07-01 13:34:28 -04:00
}
2019-10-10 00:52:42 -04:00
2022-06-10 15:51:20 +02:00
if ( result . ImagesToBuild . Count > 0 )
2021-07-01 13:34:28 -04:00
{
2022-06-10 15:51:20 +02:00
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 )));
}
2021-07-01 13:34:28 -04:00
}
2019-10-28 11:56:12 -04:00
2021-07-01 13:34:28 -04:00
#if ! OS_LINUX
2022-06-10 15:51:20 +02:00
if ( containerSetupSteps . Count > 0 )
{
executionContext . Output ( "Container action is only supported on Linux, skip pull and build docker images." );
containerSetupSteps . Clear ();
}
2021-07-01 13:34:28 -04:00
#endif
2022-06-10 15:51:20 +02:00
}
2021-07-01 13:34:28 -04:00
return new PrepareResult ( containerSetupSteps , result . PreStepTracker );
}
2020-06-02 17:21:50 -04:00
2021-07-28 15:35:21 -04:00
private async Task < PrepareActionsState > PrepareActionsRecursiveAsync ( IExecutionContext executionContext , PrepareActionsState state , IEnumerable < Pipelines . ActionStep > actions , Int32 depth = 0 , Guid parentStepId = default ( Guid ))
2021-07-01 13:34:28 -04:00
{
ArgUtil . NotNull ( executionContext , nameof ( executionContext ));
if ( depth > Constants . CompositeActionsMaxDepth )
{
throw new Exception ( $"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}" );
}
2020-06-02 17:21:50 -04:00
var repositoryActions = new List < Pipelines . ActionStep >();
2019-10-10 00:52:42 -04:00
foreach ( var action in actions )
{
if ( action . Reference . Type == Pipelines . ActionSourceType . ContainerRegistry )
{
ArgUtil . NotNull ( action , nameof ( action ));
var containerReference = action . Reference as Pipelines . ContainerRegistryReference ;
ArgUtil . NotNull ( containerReference , nameof ( containerReference ));
ArgUtil . NotNullOrEmpty ( containerReference . Image , nameof ( containerReference . Image ));
2021-07-01 13:34:28 -04:00
if (! state . ImagesToPull . ContainsKey ( containerReference . Image ))
2019-10-10 00:52:42 -04:00
{
2021-07-01 13:34:28 -04:00
state . ImagesToPull [ containerReference . Image ] = new List < Guid >();
2019-10-10 00:52:42 -04:00
}
Trace . Info ( $"Action {action.Name} ({action.Id}) needs to pull image '{containerReference.Image}'" );
2021-07-01 13:34:28 -04:00
state . ImagesToPull [ containerReference . Image ]. Add ( action . Id );
2019-10-10 00:52:42 -04:00
}
2021-07-01 13:34:28 -04:00
else if ( action . Reference . Type == Pipelines . ActionSourceType . Repository )
2020-06-02 17:21:50 -04:00
{
repositoryActions . Add ( action );
}
}
if ( repositoryActions . Count > 0 )
{
// Get the download info
var downloadInfos = await GetDownloadInfoAsync ( executionContext , repositoryActions );
// Download each action
foreach ( var action in repositoryActions )
{
var lookupKey = GetDownloadInfoLookupKey ( action );
if ( string . IsNullOrEmpty ( lookupKey ))
{
continue ;
}
if (! downloadInfos . TryGetValue ( lookupKey , out var downloadInfo ))
{
throw new Exception ( $"Missing download info for {lookupKey}" );
}
await DownloadRepositoryActionAsync ( executionContext , downloadInfo );
}
// More preparation based on content in the repository (action.yml)
foreach ( var action in repositoryActions )
{
var setupInfo = PrepareRepositoryActionAsync ( executionContext , action );
2021-07-01 13:34:28 -04:00
if ( setupInfo != null && setupInfo . Container != null )
2020-06-02 17:21:50 -04:00
{
2021-07-01 13:34:28 -04:00
if (! string . IsNullOrEmpty ( setupInfo . Container . Image ))
2020-06-02 17:21:50 -04:00
{
2021-07-01 13:34:28 -04:00
if (! state . ImagesToPull . ContainsKey ( setupInfo . Container . Image ))
2020-06-02 17:21:50 -04:00
{
2021-07-01 13:34:28 -04:00
state . ImagesToPull [ setupInfo . Container . Image ] = new List < Guid >();
2020-06-02 17:21:50 -04:00
}
2021-07-01 13:34:28 -04:00
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 );
2020-06-02 17:21:50 -04:00
}
else
{
2021-07-01 13:34:28 -04:00
ArgUtil . NotNullOrEmpty ( setupInfo . Container . ActionRepository , nameof ( setupInfo . Container . ActionRepository ));
2020-06-02 17:21:50 -04:00
2021-07-01 13:34:28 -04:00
if (! state . ImagesToBuild . ContainsKey ( setupInfo . Container . ActionRepository ))
2020-06-02 17:21:50 -04:00
{
2021-07-01 13:34:28 -04:00
state . ImagesToBuild [ setupInfo . Container . ActionRepository ] = new List < Guid >();
2020-06-02 17:21:50 -04:00
}
2021-07-01 13:34:28 -04:00
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 ;
2020-06-02 17:21:50 -04:00
}
}
2021-07-28 15:35:21 -04:00
else if ( setupInfo != null && setupInfo . Steps != null && setupInfo . Steps . Count > 0 )
2021-07-01 13:34:28 -04:00
{
2021-07-28 15:35:21 -04:00
state = await PrepareActionsRecursiveAsync ( executionContext , state , setupInfo . Steps , depth + 1 , action . Id );
2021-07-01 13:34:28 -04:00
}
2020-04-13 21:46:30 -04:00
var repoAction = action . Reference as Pipelines . RepositoryPathReference ;
if ( repoAction . RepositoryType != Pipelines . PipelineConstants . SelfAlias )
{
var definition = LoadAction ( executionContext , action );
2021-07-28 15:35:21 -04:00
if ( definition . Data . Execution . HasPre )
2020-04-13 21:46:30 -04:00
{
Trace . Info ( $"Add 'pre' execution for {action.Id}" );
2021-07-28 15:35:21 -04:00
// Root Step
if ( depth < 1 )
{
var actionRunner = HostContext . CreateService < IActionRunner >();
actionRunner . Action = action ;
actionRunner . Stage = ActionRunStage . Pre ;
actionRunner . Condition = definition . Data . Execution . InitCondition ;
state . PreStepTracker [ action . Id ] = actionRunner ;
}
// Embedded Step
2021-08-02 12:59:09 -07:00
else
2021-07-28 15:35:21 -04:00
{
if (! _cachedEmbeddedPreSteps . ContainsKey ( parentStepId ))
{
_cachedEmbeddedPreSteps [ parentStepId ] = new List < Pipelines . ActionStep >();
}
2021-08-02 14:57:25 -04:00
// Clone action so we can modify the condition without affecting the original
var clonedAction = action . Clone () as Pipelines . ActionStep ;
clonedAction . Condition = definition . Data . Execution . InitCondition ;
_cachedEmbeddedPreSteps [ parentStepId ]. Add ( clonedAction );
2021-07-28 15:35:21 -04:00
}
}
if ( definition . Data . Execution . HasPost && depth > 0 )
{
if (! _cachedEmbeddedPostSteps . ContainsKey ( parentStepId ))
{
// If we haven't done so already, add the parent to the post steps
_cachedEmbeddedPostSteps [ parentStepId ] = new Stack < Pipelines . ActionStep >();
}
2021-08-02 14:57:25 -04:00
// Clone action so we can modify the condition without affecting the original
var clonedAction = action . Clone () as Pipelines . ActionStep ;
clonedAction . Condition = definition . Data . Execution . CleanupCondition ;
_cachedEmbeddedPostSteps [ parentStepId ]. Push ( clonedAction );
2020-04-13 21:46:30 -04:00
}
}
2021-10-27 09:31:58 -04:00
else if ( depth > 0 )
{
// if we're in a composite action and haven't loaded the local action yet
// we assume it has a post step
if (! _cachedEmbeddedPostSteps . ContainsKey ( parentStepId ))
{
// If we haven't done so already, add the parent to the post steps
_cachedEmbeddedPostSteps [ parentStepId ] = new Stack < Pipelines . ActionStep >();
}
// Clone action so we can modify the condition without affecting the original
var clonedAction = action . Clone () as Pipelines . ActionStep ;
2021-11-18 17:56:13 +01:00
_cachedEmbeddedPostSteps [ parentStepId ]. Push ( clonedAction );
2021-10-27 09:31:58 -04:00
}
2019-10-10 00:52:42 -04:00
}
}
2021-07-01 13:34:28 -04:00
return state ;
2019-10-10 00:52:42 -04:00
}
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 )
{
2023-06-02 13:01:59 +02:00
if ( FeatureManager . IsContainerHooksEnabled ( executionContext . Global . Variables ))
2019-10-10 00:52:42 -04:00
{
2023-06-02 13:01:59 +02:00
Trace . Info ( "Load action that will run container through container hooks." );
var containerAction = action . Reference as Pipelines . ContainerRegistryReference ;
definition . Data . Execution = new ContainerActionExecutionData ()
{
Image = containerAction . Image ,
};
Trace . Info ( $"Using action container image: {containerAction.Image}." );
}
else
{
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
};
2019-10-10 00:52:42 -04:00
2023-06-02 13:01:59 +02:00
Trace . Info ( $"Using action container image: {container.ContainerImage}." );
}
2019-10-10 00:52:42 -04:00
}
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 ;
2020-01-20 18:22:59 +01:00
string manifestFile = Path . Combine ( actionDirectory , Constants . Path . ActionManifestYmlFile );
string manifestFileYaml = Path . Combine ( actionDirectory , Constants . Path . ActionManifestYamlFile );
2019-10-10 00:52:42 -04:00
string dockerFile = Path . Combine ( actionDirectory , "Dockerfile" );
string dockerFileLowerCase = Path . Combine ( actionDirectory , "dockerfile" );
2020-01-20 18:22:59 +01:00
if ( File . Exists ( manifestFile ) || File . Exists ( manifestFileYaml ))
2019-10-10 00:52:42 -04:00
{
var manifestManager = HostContext . GetService < IActionManifestManager >();
2020-01-20 18:22:59 +01:00
if ( File . Exists ( manifestFile ))
{
definition . Data = manifestManager . Load ( executionContext , manifestFile );
}
else
{
definition . Data = manifestManager . Load ( executionContext , manifestFileYaml );
}
2019-10-10 00:52:42 -04:00
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)}." );
}
2020-04-13 21:46:30 -04:00
if (! string . IsNullOrEmpty ( containerAction . Pre ))
{
Trace . Info ( $"Action container pre entrypoint: {containerAction.Pre}." );
}
2019-10-10 00:52:42 -04:00
if (! string . IsNullOrEmpty ( containerAction . EntryPoint ))
{
Trace . Info ( $"Action container entrypoint: {containerAction.EntryPoint}." );
}
2020-04-13 21:46:30 -04:00
if (! string . IsNullOrEmpty ( containerAction . Post ))
2019-10-10 00:52:42 -04:00
{
2020-04-13 21:46:30 -04:00
Trace . Info ( $"Action container post entrypoint: {containerAction.Post}." );
2019-10-10 00:52:42 -04:00
}
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 ;
2020-04-13 21:46:30 -04:00
Trace . Info ( $"Action pre node.js file: {nodeAction.Pre ?? " N / A "}." );
2019-10-10 00:52:42 -04:00
Trace . Info ( $"Action node.js file: {nodeAction.Script}." );
2020-04-13 21:46:30 -04:00
Trace . Info ( $"Action post node.js file: {nodeAction.Post ?? " N / A "}." );
2019-10-10 00:52:42 -04:00
}
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 ))
{
2020-04-13 21:46:30 -04:00
pluginAction . Post = plugin . PostPluginTypeName ;
2019-10-10 00:52:42 -04:00
Trace . Info ( $"Action cleanup plugin: {plugin.PluginTypeName}." );
}
}
2020-07-29 15:12:15 -04:00
else if ( definition . Data . Execution . ExecutionType == ActionExecutionType . Composite )
2020-06-23 15:35:32 -04:00
{
var compositeAction = definition . Data . Execution as CompositeActionExecutionData ;
2020-07-13 17:23:19 -04:00
Trace . Info ( $"Load {compositeAction.Steps?.Count ?? 0} action steps." );
Trace . Verbose ( $"Details: {StringUtil.ConvertToJson(compositeAction?.Steps)}" );
Trace . Info ( $"Load: {compositeAction.Outputs?.Count ?? 0} number of outputs" );
Trace . Info ( $"Details: {StringUtil.ConvertToJson(compositeAction?.Outputs)}" );
2021-07-28 15:35:21 -04:00
if ( CachedEmbeddedPreSteps . TryGetValue ( action . Id , out var preSteps ))
{
compositeAction . PreSteps = preSteps ;
}
if ( CachedEmbeddedPostSteps . TryGetValue ( action . Id , out var postSteps ))
{
compositeAction . PostSteps = postSteps ;
}
if ( _cachedEmbeddedStepIds . ContainsKey ( action . Id ))
{
for ( var i = 0 ; i < compositeAction . Steps . Count ; i ++)
{
2021-11-18 17:56:13 +01:00
// Load stored Ids for later load actions
2021-07-28 15:35:21 -04:00
compositeAction . Steps [ i ]. Id = _cachedEmbeddedStepIds [ action . Id ][ i ];
}
}
2021-11-18 17:56:13 +01:00
else
{
_cachedEmbeddedStepIds [ action . Id ] = new List < Guid >();
foreach ( var compositeStep in compositeAction . Steps )
{
var guid = Guid . NewGuid ();
compositeStep . Id = guid ;
_cachedEmbeddedStepIds [ action . Id ]. Add ( guid );
}
}
2020-06-23 15:35:32 -04:00
}
2019-10-10 00:52:42 -04:00
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.
2020-01-20 18:22:59 +01:00
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?" );
2019-10-10 00:52:42 -04:00
}
}
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 PullActionContainerAsync ( IExecutionContext executionContext , object data )
{
var setupInfo = data as ContainerSetupInfo ;
ArgUtil . NotNull ( setupInfo , nameof ( setupInfo ));
ArgUtil . NotNullOrEmpty ( setupInfo . Container . Image , nameof ( setupInfo . Container . Image ));
2020-07-22 14:55:49 -04:00
executionContext . Output ( $"##[group]Pull down action image '{setupInfo.Container.Image}'" );
2019-10-10 00:52:42 -04:00
// Pull down docker image with retry up to 3 times
2021-06-04 16:51:30 +02:00
var dockerManager = HostContext . GetService < IDockerCommandManager >();
2019-10-10 00:52:42 -04:00
int retryCount = 0 ;
int pullExitCode = 0 ;
while ( retryCount < 3 )
{
2021-06-04 16:51:30 +02:00
pullExitCode = await dockerManager . DockerPull ( executionContext , setupInfo . Container . Image );
2019-10-10 00:52:42 -04:00
if ( pullExitCode == 0 )
{
break ;
}
else
{
retryCount ++;
if ( retryCount < 3 )
{
var backOff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 1 ), TimeSpan . FromSeconds ( 10 ));
executionContext . Warning ( $"Docker pull failed with exit code {pullExitCode}, back off {backOff.TotalSeconds} seconds before retry." );
await Task . Delay ( backOff );
}
}
}
2020-08-14 17:53:30 +02:00
executionContext . Output ( "##[endgroup]" );
2019-10-10 00:52:42 -04:00
if ( retryCount == 3 && pullExitCode != 0 )
{
throw new InvalidOperationException ( $"Docker pull failed with exit code {pullExitCode}" );
}
foreach ( var stepId in setupInfo . StepIds )
{
CachedActionContainers [ stepId ] = new ContainerInfo () { ContainerImage = setupInfo . Container . Image };
Trace . Info ( $"Prepared docker image '{setupInfo.Container.Image}' for action {stepId} ({setupInfo.Container.Image})" );
}
}
private async Task BuildActionContainerAsync ( IExecutionContext executionContext , object data )
{
var setupInfo = data as ContainerSetupInfo ;
ArgUtil . NotNull ( setupInfo , nameof ( setupInfo ));
ArgUtil . NotNullOrEmpty ( setupInfo . Container . Dockerfile , nameof ( setupInfo . Container . Dockerfile ));
2020-07-22 14:55:49 -04:00
executionContext . Output ( $"##[group]Build container for action use: '{setupInfo.Container.Dockerfile}'." );
2019-10-10 00:52:42 -04:00
// Build docker image with retry up to 3 times
2021-06-04 16:51:30 +02:00
var dockerManager = HostContext . GetService < IDockerCommandManager >();
2019-10-10 00:52:42 -04:00
int retryCount = 0 ;
int buildExitCode = 0 ;
2021-06-04 16:51:30 +02:00
var imageName = $"{dockerManager.DockerInstanceLabel}:{Guid.NewGuid().ToString(" N ")}" ;
2019-10-10 00:52:42 -04:00
while ( retryCount < 3 )
{
2021-06-04 16:51:30 +02:00
buildExitCode = await dockerManager . DockerBuild (
2020-05-11 13:57:31 -04:00
executionContext ,
setupInfo . Container . WorkingDirectory ,
setupInfo . Container . Dockerfile ,
Directory . GetParent ( setupInfo . Container . Dockerfile ). FullName ,
imageName );
2019-10-10 00:52:42 -04:00
if ( buildExitCode == 0 )
{
break ;
}
else
{
retryCount ++;
if ( retryCount < 3 )
{
var backOff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 1 ), TimeSpan . FromSeconds ( 10 ));
executionContext . Warning ( $"Docker build failed with exit code {buildExitCode}, back off {backOff.TotalSeconds} seconds before retry." );
await Task . Delay ( backOff );
}
}
}
2020-07-22 14:55:49 -04:00
executionContext . Output ( "##[endgroup]" );
2019-10-10 00:52:42 -04:00
if ( retryCount == 3 && buildExitCode != 0 )
{
throw new InvalidOperationException ( $"Docker build failed with exit code {buildExitCode}" );
}
foreach ( var stepId in setupInfo . StepIds )
{
CachedActionContainers [ stepId ] = new ContainerInfo () { ContainerImage = imageName };
Trace . Info ( $"Prepared docker image '{imageName}' for action {stepId} ({setupInfo.Container.Dockerfile})" );
}
}
2020-06-02 17:21:50 -04:00
// This implementation is temporary and will be replaced with a REST API call to the service to resolve
2020-06-09 08:53:28 -04:00
private async Task < IDictionary < string , WebApi . ActionDownloadInfo >> GetDownloadInfoAsync ( IExecutionContext executionContext , List < Pipelines . ActionStep > actions )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
executionContext . Output ( "Getting action download info" );
2020-06-02 17:21:50 -04:00
2020-06-09 08:53:28 -04:00
// Convert to action reference
var actionReferences = actions
. GroupBy ( x => GetDownloadInfoLookupKey ( x ))
. Where ( x => ! string . IsNullOrEmpty ( x . Key ))
. Select ( x =>
{
var action = x . First ();
var repositoryReference = action . Reference as Pipelines . RepositoryPathReference ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ));
return new WebApi . ActionReference
{
NameWithOwner = repositoryReference . Name ,
Ref = repositoryReference . Ref ,
2021-08-11 09:48:32 -04:00
Path = repositoryReference . Path ,
2020-06-09 08:53:28 -04:00
};
})
. ToList ();
// Nothing to resolve?
if ( actionReferences . Count == 0 )
{
return new Dictionary < string , WebApi . ActionDownloadInfo >();
}
// Resolve download info
2023-05-03 16:04:21 -04:00
var launchServer = HostContext . GetService < ILaunchServer >();
2020-06-09 08:53:28 -04:00
var jobServer = HostContext . GetService < IJobServer >();
var actionDownloadInfos = default ( WebApi . ActionDownloadInfoCollection );
for ( var attempt = 1 ; attempt <= 3 ; attempt ++)
{
try
{
2023-05-03 16:04:21 -04:00
if ( MessageUtil . IsRunServiceJob ( executionContext . Global . Variables . Get ( Constants . Variables . System . JobRequestType )))
{
actionDownloadInfos = await launchServer . ResolveActionsDownloadInfoAsync ( executionContext . Global . Plan . PlanId , executionContext . Root . Id , new WebApi . ActionReferenceList { Actions = actionReferences }, executionContext . CancellationToken );
}
else
{
actionDownloadInfos = await jobServer . ResolveActionDownloadInfoAsync ( executionContext . Global . Plan . ScopeIdentifier , executionContext . Global . Plan . PlanType , executionContext . Global . Plan . PlanId , executionContext . Root . Id , new WebApi . ActionReferenceList { Actions = actionReferences }, executionContext . CancellationToken );
}
2020-06-09 08:53:28 -04:00
break ;
}
2022-02-23 15:48:24 +01:00
catch ( Exception ex ) when (! executionContext . CancellationToken . IsCancellationRequested ) // Do not retry if the run is cancelled.
2020-06-09 08:53:28 -04:00
{
2021-09-21 09:59:21 -04:00
// UnresolvableActionDownloadInfoException is a 422 client error, don't retry
2024-03-21 11:50:45 -07:00
// NonRetryableActionDownloadInfoException is an non-retryable exception from Actions
2021-09-21 09:59:21 -04:00
// Some possible cases are:
// * Repo is rate limited
// * Repo or tag doesn't exist, or isn't public
// * Policy validation failed
2024-03-21 11:50:45 -07:00
if ( attempt < 3 && !( ex is WebApi . UnresolvableActionDownloadInfoException ) && !( ex is WebApi . NonRetryableActionDownloadInfoException ))
2020-06-09 08:53:28 -04:00
{
2020-12-11 11:07:43 -05:00
executionContext . Output ( $"Failed to resolve action download info. Error: {ex.Message}" );
executionContext . Debug ( ex . ToString ());
if ( String . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF" )))
{
var backoff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 10 ), TimeSpan . FromSeconds ( 30 ));
executionContext . Output ( $"Retrying in {backoff.TotalSeconds} seconds" );
await Task . Delay ( backoff );
}
}
else
{
2021-01-05 16:08:02 -05:00
// Some possible cases are:
// * Repo is rate limited
// * Repo or tag doesn't exist, or isn't public
2021-09-21 09:59:21 -04:00
// * Policy validation failed
2021-01-05 16:08:02 -05:00
if ( ex is WebApi . UnresolvableActionDownloadInfoException )
{
throw ;
}
else
{
// This exception will be traced as an infrastructure failure
throw new WebApi . FailedToResolveActionDownloadInfoException ( "Failed to resolve action download info." , ex );
}
2020-06-09 08:53:28 -04:00
}
}
}
ArgUtil . NotNull ( actionDownloadInfos , nameof ( actionDownloadInfos ));
ArgUtil . NotNull ( actionDownloadInfos . Actions , nameof ( actionDownloadInfos . Actions ));
var defaultAccessToken = executionContext . GetGitHubContext ( "token" );
2020-06-02 17:21:50 -04:00
2020-06-09 08:53:28 -04:00
foreach ( var actionDownloadInfo in actionDownloadInfos . Actions . Values )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
// Add secret
HostContext . SecretMasker . AddValue ( actionDownloadInfo . Authentication ?. Token );
2020-06-02 17:21:50 -04:00
2020-06-15 13:13:47 -04:00
// Default auth token
if ( string . IsNullOrEmpty ( actionDownloadInfo . Authentication ?. Token ))
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
actionDownloadInfo . Authentication = new WebApi . ActionDownloadAuthentication { Token = defaultAccessToken };
2020-06-02 17:21:50 -04:00
}
}
2020-06-09 08:53:28 -04:00
return actionDownloadInfos . Actions ;
2020-06-02 17:21:50 -04:00
}
2020-06-09 08:53:28 -04:00
private async Task DownloadRepositoryActionAsync ( IExecutionContext executionContext , WebApi . ActionDownloadInfo downloadInfo )
2020-06-02 17:21:50 -04:00
{
Trace . Entering ();
ArgUtil . NotNull ( executionContext , nameof ( executionContext ));
ArgUtil . NotNull ( downloadInfo , nameof ( downloadInfo ));
ArgUtil . NotNullOrEmpty ( downloadInfo . NameWithOwner , nameof ( downloadInfo . NameWithOwner ));
ArgUtil . NotNullOrEmpty ( downloadInfo . Ref , nameof ( downloadInfo . Ref ));
2023-09-28 19:43:55 -04:00
ArgUtil . NotNullOrEmpty ( downloadInfo . Ref , nameof ( downloadInfo . ResolvedNameWithOwner ));
ArgUtil . NotNullOrEmpty ( downloadInfo . Ref , nameof ( downloadInfo . ResolvedSha ));
2020-06-02 17:21:50 -04:00
string destDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), downloadInfo . NameWithOwner . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ), downloadInfo . Ref );
string watermarkFile = GetWatermarkFilePath ( destDirectory );
if ( File . Exists ( watermarkFile ))
{
executionContext . Debug ( $"Action '{downloadInfo.NameWithOwner}@{downloadInfo.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 );
2025-01-15 11:25:12 -06:00
if ( downloadInfo . PackageDetails != null )
{
executionContext . Output ( $"##[group]Download immutable action package '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'" );
executionContext . Output ( $"Version: {downloadInfo.PackageDetails.Version}" );
executionContext . Output ( $"Digest: {downloadInfo.PackageDetails.ManifestDigest}" );
executionContext . Output ( $"Source commit SHA: {downloadInfo.ResolvedSha}" );
executionContext . Output ( "##[endgroup]" );
}
else
{
executionContext . Output ( $"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}' (SHA:{downloadInfo.ResolvedSha})" );
}
2020-06-02 17:21:50 -04:00
}
2019-10-10 00:52:42 -04:00
//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" );
2021-07-01 13:34:28 -04:00
string link = downloadInfo ?. ZipballUrl ;
2019-10-10 00:52:42 -04:00
#else
string archiveFile = Path . Combine ( tempDirectory , $"{Guid.NewGuid()}.tar.gz" );
2021-07-01 13:34:28 -04:00
string link = downloadInfo ?. TarballUrl ;
2019-10-10 00:52:42 -04:00
#endif
2020-04-23 17:02:13 -04:00
2019-10-10 00:52:42 -04:00
try
{
2023-09-28 19:43:55 -04:00
var useActionArchiveCache = false ;
2024-04-16 16:52:10 -04:00
var hasActionArchiveCache = false ;
var actionArchiveCacheDir = Environment . GetEnvironmentVariable ( Constants . Variables . Agent . ActionArchiveCacheDirectory );
if (! string . IsNullOrEmpty ( actionArchiveCacheDir ) &&
Directory . Exists ( actionArchiveCacheDir ))
2019-10-10 00:52:42 -04:00
{
2024-04-16 16:52:10 -04:00
hasActionArchiveCache = true ;
Trace . Info ( $"Check if action archive '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}' already exists in cache directory '{actionArchiveCacheDir}'" );
2023-09-28 19:43:55 -04:00
#if OS_WINDOWS
2024-04-16 16:52:10 -04:00
var cacheArchiveFile = Path . Combine ( actionArchiveCacheDir , downloadInfo . ResolvedNameWithOwner . Replace ( Path . DirectorySeparatorChar , '_' ). Replace ( Path . AltDirectorySeparatorChar , '_' ), $"{downloadInfo.ResolvedSha}.zip" );
2023-09-28 19:43:55 -04:00
#else
2024-04-16 16:52:10 -04:00
var cacheArchiveFile = Path . Combine ( actionArchiveCacheDir , downloadInfo . ResolvedNameWithOwner . Replace ( Path . DirectorySeparatorChar , '_' ). Replace ( Path . AltDirectorySeparatorChar , '_' ), $"{downloadInfo.ResolvedSha}.tar.gz" );
2023-09-28 19:43:55 -04:00
#endif
2024-04-16 16:52:10 -04:00
if ( File . Exists ( cacheArchiveFile ))
{
try
2019-10-10 00:52:42 -04:00
{
2024-04-16 16:52:10 -04:00
Trace . Info ( $"Found action archive '{cacheArchiveFile}' in cache directory '{actionArchiveCacheDir}'" );
File . Copy ( cacheArchiveFile , archiveFile );
useActionArchiveCache = true ;
executionContext . Debug ( $"Copied action archive '{cacheArchiveFile}' to '{archiveFile}'" );
}
catch ( Exception ex )
{
Trace . Error ( $"Failed to copy action archive '{cacheArchiveFile}' to '{archiveFile}'. Error: {ex}" );
2019-10-10 00:52:42 -04:00
}
}
}
2024-04-16 16:52:10 -04:00
executionContext . Global . JobTelemetry . Add ( new JobTelemetry ()
{
Type = JobTelemetryType . General ,
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache}"
});
2023-09-28 19:43:55 -04:00
if (! useActionArchiveCache )
{
await DownloadRepositoryArchive ( executionContext , link , downloadInfo . Authentication ?. Token , archiveFile );
}
2019-10-10 00:52:42 -04:00
var stagingDirectory = Path . Combine ( tempDirectory , "_staging" );
Directory . CreateDirectory ( stagingDirectory );
#if OS_WINDOWS
2023-09-06 09:32:57 -04:00
try
{
ZipFile . ExtractToDirectory ( archiveFile , stagingDirectory );
}
catch ( InvalidDataException e )
{
throw new InvalidActionArchiveException ( $"Can't un-zip archive file: {archiveFile}. action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. error: {e}." );
}
2019-10-10 00:52:42 -04:00
#else
string tar = WhichUtil . Which ( "tar" , require : true , trace : Trace );
// tar -xzf
using ( var processInvoker = HostContext . CreateService < IProcessInvoker >())
{
2023-10-19 08:15:06 -04:00
var tarOutputs = new List < string >();
2019-10-10 00:52:42 -04:00
processInvoker . OutputDataReceived += new EventHandler < ProcessDataReceivedEventArgs >(( sender , args ) =>
{
if (! string . IsNullOrEmpty ( args . Data ))
{
Trace . Info ( args . Data );
2023-10-19 08:15:06 -04:00
tarOutputs . Add ( $"STDOUT: {args.Data}" );
2019-10-10 00:52:42 -04:00
}
});
processInvoker . ErrorDataReceived += new EventHandler < ProcessDataReceivedEventArgs >(( sender , args ) =>
{
if (! string . IsNullOrEmpty ( args . Data ))
{
Trace . Error ( args . Data );
2023-10-19 08:15:06 -04:00
tarOutputs . Add ( $"STDERR: {args.Data}" );
2019-10-10 00:52:42 -04:00
}
});
int exitCode = await processInvoker . ExecuteAsync ( stagingDirectory , tar , $"-xzf \" { archiveFile } \ "" , null , executionContext . CancellationToken );
if ( exitCode != 0 )
{
2024-04-16 16:52:10 -04:00
var fileInfo = new FileInfo ( archiveFile );
var sha256hash = await IOUtil . GetFileContentSha256HashAsync ( archiveFile );
throw new InvalidActionArchiveException ( $"Can't use 'tar -xzf' extract archive file: {archiveFile} (SHA256 '{sha256hash}', size '{fileInfo.Length}' bytes, tar outputs '{string.Join(' ', tarOutputs)}'). Action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. return code: {exitCode}." );
2019-10-10 00:52:42 -04:00
}
}
#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." );
2020-03-24 21:51:37 -04:00
File . WriteAllText ( watermarkFile , DateTime . UtcNow . ToString ());
2019-10-10 00:52:42 -04:00
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 );
}
}
}
2020-04-23 17:02:13 -04:00
private string GetWatermarkFilePath ( string directory ) => directory + ".completed" ;
2021-07-01 13:34:28 -04:00
private ActionSetupInfo PrepareRepositoryActionAsync ( IExecutionContext executionContext , Pipelines . ActionStep repositoryAction )
2019-10-10 00:52:42 -04:00
{
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 ;
}
2021-07-01 13:34:28 -04:00
var setupInfo = new ActionSetupInfo ();
var actionContainer = new ActionContainer ();
2019-10-10 00:52:42 -04:00
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}" ;
2021-07-01 13:34:28 -04:00
actionContainer . ActionRepository = $"{repositoryReference.Name}/{repositoryReference.Path}@{repositoryReference.Ref}" ;
2019-10-10 00:52:42 -04:00
}
else
{
2021-07-01 13:34:28 -04:00
actionContainer . ActionRepository = $"{repositoryReference.Name}@{repositoryReference.Ref}" ;
2019-10-10 00:52:42 -04:00
}
// find the docker file or action.yml file
var dockerFile = Path . Combine ( actionEntryDirectory , "Dockerfile" );
var dockerFileLowerCase = Path . Combine ( actionEntryDirectory , "dockerfile" );
2020-01-20 18:22:59 +01:00
var actionManifest = Path . Combine ( actionEntryDirectory , Constants . Path . ActionManifestYmlFile );
var actionManifestYaml = Path . Combine ( actionEntryDirectory , Constants . Path . ActionManifestYamlFile );
if ( File . Exists ( actionManifest ) || File . Exists ( actionManifestYaml ))
2019-10-10 00:52:42 -04:00
{
executionContext . Debug ( $"action.yml for action: '{actionManifest}'." );
var manifestManager = HostContext . GetService < IActionManifestManager >();
2020-01-20 18:22:59 +01:00
ActionDefinitionData actionDefinitionData = null ;
if ( File . Exists ( actionManifest ))
{
actionDefinitionData = manifestManager . Load ( executionContext , actionManifest );
}
else
{
actionDefinitionData = manifestManager . Load ( executionContext , actionManifestYaml );
}
2019-10-10 00:52:42 -04:00
if ( actionDefinitionData . Execution . ExecutionType == ActionExecutionType . Container )
{
var containerAction = actionDefinitionData . Execution as ContainerActionExecutionData ;
2023-06-17 03:53:52 +02:00
if ( DockerUtil . IsDockerfile ( containerAction . Image ))
2019-10-10 00:52:42 -04:00
{
var dockerFileFullPath = Path . Combine ( actionEntryDirectory , containerAction . Image );
executionContext . Debug ( $"Dockerfile for action: '{dockerFileFullPath}'." );
2021-07-01 13:34:28 -04:00
actionContainer . Dockerfile = dockerFileFullPath ;
actionContainer . WorkingDirectory = destDirectory ;
setupInfo . Container = actionContainer ;
2019-10-10 00:52:42 -04:00
return setupInfo ;
}
else if ( containerAction . Image . StartsWith ( "docker://" , StringComparison . OrdinalIgnoreCase ))
{
var actionImage = containerAction . Image . Substring ( "docker://" . Length );
executionContext . Debug ( $"Container image for action: '{actionImage}'." );
2021-07-01 13:34:28 -04:00
actionContainer . Image = actionImage ;
setupInfo . Container = actionContainer ;
2019-10-10 00:52:42 -04:00
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 ;
}
2020-07-29 15:12:15 -04:00
else if ( actionDefinitionData . Execution . ExecutionType == ActionExecutionType . Composite )
2020-06-23 15:35:32 -04:00
{
2021-07-01 13:34:28 -04:00
Trace . Info ( $"Loading Composite steps" );
var compositeAction = actionDefinitionData . Execution as CompositeActionExecutionData ;
setupInfo . Steps = compositeAction . Steps ;
2021-07-28 15:35:21 -04:00
// cache steps ids if not done so already
if (! _cachedEmbeddedStepIds . ContainsKey ( repositoryAction . Id ))
{
_cachedEmbeddedStepIds [ repositoryAction . Id ] = new List < Guid >();
foreach ( var compositeStep in compositeAction . Steps )
{
var guid = Guid . NewGuid ();
compositeStep . Id = guid ;
_cachedEmbeddedStepIds [ repositoryAction . Id ]. Add ( guid );
}
}
2021-07-01 13:34:28 -04:00
return setupInfo ;
2020-06-23 15:35:32 -04:00
}
2019-10-10 00:52:42 -04:00
else
{
throw new NotSupportedException ( actionDefinitionData . Execution . ExecutionType . ToString ());
}
}
else if ( File . Exists ( dockerFile ))
{
executionContext . Debug ( $"Dockerfile for action: '{dockerFile}'." );
2021-07-01 13:34:28 -04:00
actionContainer . Dockerfile = dockerFile ;
actionContainer . WorkingDirectory = destDirectory ;
setupInfo . Container = actionContainer ;
2019-10-10 00:52:42 -04:00
return setupInfo ;
}
else if ( File . Exists ( dockerFileLowerCase ))
{
executionContext . Debug ( $"Dockerfile for action: '{dockerFileLowerCase}'." );
2021-07-01 13:34:28 -04:00
actionContainer . Dockerfile = dockerFileLowerCase ;
actionContainer . WorkingDirectory = destDirectory ;
setupInfo . Container = actionContainer ;
2019-10-10 00:52:42 -04:00
return setupInfo ;
}
else
{
2023-06-29 11:33:46 +02:00
var reference = repositoryReference . Name ;
if (! string . IsNullOrEmpty ( repositoryReference . Path ))
{
reference = $"{reference}/{repositoryReference.Path}" ;
}
if (! string . IsNullOrEmpty ( repositoryReference . Ref ))
{
reference = $"{reference}@{repositoryReference.Ref}" ;
}
throw new InvalidOperationException ( $"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' for action '{reference}'." );
2019-10-10 00:52:42 -04:00
}
}
2020-05-11 12:23:02 -04:00
2020-06-02 17:21:50 -04:00
private static string GetDownloadInfoLookupKey ( Pipelines . ActionStep action )
{
if ( action . Reference . Type != Pipelines . ActionSourceType . Repository )
{
return null ;
}
var repositoryReference = action . Reference as Pipelines . RepositoryPathReference ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ));
if ( string . Equals ( repositoryReference . RepositoryType , Pipelines . PipelineConstants . SelfAlias , StringComparison . OrdinalIgnoreCase ))
{
return null ;
}
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 ));
return $"{repositoryReference.Name}@{repositoryReference.Ref}" ;
}
2020-06-09 08:53:28 -04:00
private AuthenticationHeaderValue CreateAuthHeader ( string token )
{
if ( string . IsNullOrEmpty ( token ))
2020-06-02 17:21:50 -04:00
{
return null ;
}
2020-06-09 08:53:28 -04:00
var base64EncodingToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"x-access-token:{token}" ));
HostContext . SecretMasker . AddValue ( base64EncodingToken );
return new AuthenticationHeaderValue ( "Basic" , base64EncodingToken );
2020-06-02 17:21:50 -04:00
}
2023-09-28 19:43:55 -04:00
private async Task DownloadRepositoryArchive ( IExecutionContext executionContext , string downloadUrl , string downloadAuthToken , string archiveFile )
{
Trace . Info ( $"Save archive '{downloadUrl}' into {archiveFile}." );
int retryCount = 0 ;
// Allow up to 20 * 60s for any action to be downloaded from github graph.
int timeoutSeconds = 20 * 60 ;
while ( retryCount < 3 )
{
2024-08-27 12:05:26 -04:00
string requestId = string . Empty ;
2023-09-28 19:43:55 -04:00
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 ( archiveFile , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize : _defaultFileStreamBufferSize , useAsync : true ))
using ( var httpClientHandler = HostContext . CreateHttpClientHandler ())
using ( var httpClient = new HttpClient ( httpClientHandler ))
{
httpClient . DefaultRequestHeaders . Authorization = CreateAuthHeader ( downloadAuthToken );
httpClient . DefaultRequestHeaders . UserAgent . AddRange ( HostContext . UserAgents );
using ( var response = await httpClient . GetAsync ( downloadUrl ))
{
2024-08-27 12:05:26 -04:00
requestId = UrlUtil . GetGitHubRequestId ( response . Headers );
2023-09-28 19:43:55 -04:00
if (! string . IsNullOrEmpty ( requestId ))
{
Trace . Info ( $"Request URL: {downloadUrl} X-GitHub-Request-Id: {requestId} Http Status: {response.StatusCode}" );
}
if ( response . IsSuccessStatusCode )
{
using ( var result = await response . Content . ReadAsStreamAsync ())
{
await result . CopyToAsync ( fs , _defaultCopyBufferSize , actionDownloadCancellation . Token );
await fs . FlushAsync ( actionDownloadCancellation . Token );
// download succeed, break out the retry loop.
break ;
}
}
else if ( response . StatusCode == HttpStatusCode . NotFound )
{
// It doesn't make sense to retry in this case, so just stop
throw new ActionNotFoundException ( new Uri ( downloadUrl ), requestId );
}
else
{
// Something else bad happened, let's go to our retry logic
response . EnsureSuccessStatusCode ();
}
}
}
}
catch ( OperationCanceledException ) when ( executionContext . CancellationToken . IsCancellationRequested )
{
Trace . Info ( "Action download has been cancelled." );
throw ;
}
catch ( OperationCanceledException ex ) when (! executionContext . CancellationToken . IsCancellationRequested && retryCount >= 2 )
{
Trace . Info ( $"Action download final retry timeout after {timeoutSeconds} seconds." );
2024-08-27 12:05:26 -04:00
throw new TimeoutException ( $"Action '{downloadUrl}' download has timed out. Error: {ex.Message} {requestId}" );
2023-09-28 19:43:55 -04:00
}
catch ( ActionNotFoundException )
{
Trace . Info ( $"The action at '{downloadUrl}' does not exist" );
throw ;
}
catch ( Exception ex ) when ( retryCount < 2 )
{
retryCount ++;
Trace . Error ( $"Fail to download archive '{downloadUrl}' -- Attempt: {retryCount}" );
Trace . Error ( ex );
if ( actionDownloadTimeout . Token . IsCancellationRequested )
{
// action download didn't finish within timeout
2024-08-27 12:05:26 -04:00
executionContext . Warning ( $"Action '{downloadUrl}' didn't finish download within {timeoutSeconds} seconds. {requestId}" );
2023-09-28 19:43:55 -04:00
}
else
{
2024-08-27 12:05:26 -04:00
executionContext . Warning ( $"Failed to download action '{downloadUrl}'. Error: {ex.Message} {requestId}" );
2023-09-28 19:43:55 -04:00
}
}
}
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 '{downloadUrl}' to '{archiveFile}'" );
}
2019-10-10 00:52:42 -04:00
}
public sealed class Definition
{
public ActionDefinitionData Data { get ; set ; }
public string Directory { get ; set ; }
}
public sealed class ActionDefinitionData
{
public string Name { get ; set ; }
public string Description { get ; set ; }
public MappingToken Inputs { get ; set ; }
public ActionExecutionData Execution { get ; set ; }
public Dictionary < String , String > Deprecated { get ; set ; }
}
public enum ActionExecutionType
{
Container ,
NodeJS ,
Plugin ,
Script ,
2020-06-23 15:35:32 -04:00
Composite ,
2019-10-10 00:52:42 -04:00
}
public sealed class ContainerActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . Container ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => ! string . IsNullOrEmpty ( Pre );
public override bool HasPost => ! string . IsNullOrEmpty ( Post );
2019-10-10 00:52:42 -04:00
public string Image { get ; set ; }
public string EntryPoint { get ; set ; }
public SequenceToken Arguments { get ; set ; }
public MappingToken Environment { get ; set ; }
2020-04-13 21:46:30 -04:00
public string Pre { get ; set ; }
public string Post { get ; set ; }
2019-10-10 00:52:42 -04:00
}
public sealed class NodeJSActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . NodeJS ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => ! string . IsNullOrEmpty ( Pre );
public override bool HasPost => ! string . IsNullOrEmpty ( Post );
2019-10-10 00:52:42 -04:00
public string Script { get ; set ; }
2020-04-13 21:46:30 -04:00
public string Pre { get ; set ; }
public string Post { get ; set ; }
2021-11-18 15:25:33 -05:00
public string NodeVersion { get ; set ; }
2019-10-10 00:52:42 -04:00
}
public sealed class PluginActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . Plugin ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => false ;
public override bool HasPost => ! string . IsNullOrEmpty ( Post );
2019-10-10 00:52:42 -04:00
public string Plugin { get ; set ; }
2020-04-13 21:46:30 -04:00
public string Post { get ; set ; }
2019-10-10 00:52:42 -04:00
}
public sealed class ScriptActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . Script ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => false ;
public override bool HasPost => false ;
2019-10-10 00:52:42 -04:00
}
2020-06-23 15:35:32 -04:00
public sealed class CompositeActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . Composite ;
2021-07-28 15:35:21 -04:00
public override bool HasPre => PreSteps . Count > 0 ;
public override bool HasPost => PostSteps . Count > 0 ;
public List < Pipelines . ActionStep > PreSteps { get ; set ; }
2020-06-23 15:35:32 -04:00
public List < Pipelines . ActionStep > Steps { get ; set ; }
2021-07-28 15:35:21 -04:00
public Stack < Pipelines . ActionStep > PostSteps { get ; set ; }
2020-07-13 17:23:19 -04:00
public MappingToken Outputs { get ; set ; }
2020-06-23 15:35:32 -04:00
}
2019-10-10 00:52:42 -04:00
public abstract class ActionExecutionData
{
2020-04-13 21:46:30 -04:00
private string _initCondition = $"{Constants.Expressions.Always}()" ;
2019-10-10 00:52:42 -04:00
private string _cleanupCondition = $"{Constants.Expressions.Always}()" ;
public abstract ActionExecutionType ExecutionType { get ; }
2020-04-13 21:46:30 -04:00
public abstract bool HasPre { get ; }
public abstract bool HasPost { get ; }
2019-10-10 00:52:42 -04:00
public string CleanupCondition
{
get { return _cleanupCondition ; }
set { _cleanupCondition = value ; }
}
2020-04-13 21:46:30 -04:00
public string InitCondition
{
get { return _initCondition ; }
set { _initCondition = value ; }
}
2019-10-10 00:52:42 -04:00
}
public class ContainerSetupInfo
{
public ContainerSetupInfo ( List < Guid > ids , string image )
{
StepIds = ids ;
Container = new ActionContainer ()
{
Image = image
};
}
public ContainerSetupInfo ( List < Guid > ids , string dockerfile , string workingDirectory )
{
StepIds = ids ;
Container = new ActionContainer ()
{
Dockerfile = dockerfile ,
WorkingDirectory = workingDirectory
};
}
public List < Guid > StepIds { get ; set ; }
public ActionContainer Container { get ; set ; }
}
public class ActionContainer
{
public string Image { get ; set ; }
public string Dockerfile { get ; set ; }
public string WorkingDirectory { get ; set ; }
public string ActionRepository { get ; set ; }
}
2021-07-01 13:34:28 -04:00
public class ActionSetupInfo
{
public ActionContainer Container { get ; set ; }
2021-08-02 12:59:09 -07:00
public List < Pipelines . ActionStep > Steps { get ; set ; }
2021-07-01 13:34:28 -04:00
}
public class PrepareActionsState
{
public Dictionary < string , List < Guid >> ImagesToPull ;
public Dictionary < string , List < Guid >> ImagesToBuild ;
public Dictionary < string , ActionContainer > ImagesToBuildInfo ;
public Dictionary < Guid , IActionRunner > PreStepTracker ;
}
2019-10-10 00:52:42 -04:00
}