2020-04-23 17:02:13 -04: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 ;
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 ;
using GitHub.Runner.Common ;
using GitHub.Runner.Sdk ;
using GitHub.Runner.Worker.Container ;
using GitHub.Services.Common ;
2020-06-09 08:53:28 -04:00
using WebApi = GitHub . DistributedTask . WebApi ;
2019-10-10 00:52:42 -04:00
using Pipelines = GitHub . DistributedTask . Pipelines ;
using PipelineTemplateConstants = GitHub . DistributedTask . Pipelines . ObjectTemplating . PipelineTemplateConstants ;
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 ;
2020-05-11 12:23:02 -04:00
private const string _dotcomApiUrl = "https://api.github.com" ;
2019-10-10 00:52:42 -04:00
2021-07-28 15:35:21 -04:00
private readonly Dictionary < Guid , ContainerInfo > _cachedActionContainers = new Dictionary < Guid , ContainerInfo > ( ) ;
2019-10-10 00:52:42 -04:00
public Dictionary < Guid , ContainerInfo > CachedActionContainers = > _cachedActionContainers ;
2021-07-28 15:35:21 -04:00
private readonly Dictionary < Guid , List < Pipelines . ActionStep > > _cachedEmbeddedPreSteps = new Dictionary < Guid , List < Pipelines . ActionStep > > ( ) ;
public Dictionary < Guid , List < Pipelines . ActionStep > > CachedEmbeddedPreSteps = > _cachedEmbeddedPreSteps ;
private readonly Dictionary < Guid , List < Guid > > _cachedEmbeddedStepIds = new Dictionary < Guid , List < Guid > > ( ) ;
public Dictionary < Guid , List < Guid > > CachedEmbeddedStepIds = > _cachedEmbeddedStepIds ;
private readonly Dictionary < Guid , Stack < Pipelines . ActionStep > > _cachedEmbeddedPostSteps = new Dictionary < Guid , Stack < Pipelines . ActionStep > > ( ) ;
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
{
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 ;
}
2019-10-10 00:52:42 -04:00
IEnumerable < Pipelines . ActionStep > actions = steps . OfType < Pipelines . ActionStep > ( ) ;
2021-07-01 13:34:28 -04:00
executionContext . Output ( "Prepare all required actions" ) ;
2021-07-28 15:35:21 -04:00
var result = await PrepareActionsRecursiveAsync ( executionContext , state , actions , depth , rootStepId ) ;
2021-07-01 13:34:28 -04:00
if ( state . ImagesToPull . Count > 0 )
2019-10-10 00:52:42 -04:00
{
2021-07-01 13:34:28 -04: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 ) ) ) ;
}
2019-10-10 00:52:42 -04:00
}
2021-07-01 13:34:28 -04:00
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 ) ) ) ;
}
}
2019-10-28 11:56:12 -04:00
2021-07-01 13:34:28 -04:00
#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 ) ;
}
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
}
}
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 )
{
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 ;
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 + + )
{
// Store Id's for later load actions
compositeAction . Steps [ i ] . Id = _cachedEmbeddedStepIds [ action . Id ] [ i ] ;
if ( string . IsNullOrEmpty ( executionContext . Global . Variables . Get ( "DistributedTask.EnableCompositeActions" ) ) & & compositeAction . Steps [ i ] . Reference . Type ! = Pipelines . ActionSourceType . Script )
{
throw new Exception ( "`uses:` keyword is not currently supported." ) ;
}
}
}
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 ( ) ;
2020-06-02 17:21:50 -04:00
2020-06-09 08:53:28 -04:00
// Nothing to resolve?
if ( actionReferences . Count = = 0 )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
return new Dictionary < string , WebApi . ActionDownloadInfo > ( ) ;
}
// Resolve download info
var jobServer = HostContext . GetService < IJobServer > ( ) ;
var actionDownloadInfos = default ( WebApi . ActionDownloadInfoCollection ) ;
for ( var attempt = 1 ; attempt < = 3 ; attempt + + )
{
try
2020-06-02 17:21:50 -04:00
{
2020-07-19 19:05:47 -04:00
actionDownloadInfos = await jobServer . ResolveActionDownloadInfoAsync ( executionContext . Global . Plan . ScopeIdentifier , executionContext . Global . Plan . PlanType , executionContext . Global . Plan . PlanId , new WebApi . ActionReferenceList { Actions = actionReferences } , executionContext . CancellationToken ) ;
2020-06-09 08:53:28 -04:00
break ;
2020-06-02 17:21:50 -04:00
}
2021-01-05 16:08:02 -05:00
catch ( Exception ex ) when ( ! executionContext . CancellationToken . IsCancellationRequested ) // Do not retry if the run is canceled.
2020-06-09 08:53:28 -04:00
{
2020-12-11 11:07:43 -05:00
if ( attempt < 3 )
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
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
}
}
}
2020-06-02 17:21:50 -04:00
2020-06-09 08:53:28 -04:00
ArgUtil . NotNull ( actionDownloadInfos , nameof ( actionDownloadInfos ) ) ;
ArgUtil . NotNull ( actionDownloadInfos . Actions , nameof ( actionDownloadInfos . Actions ) ) ;
var apiUrl = GetApiUrl ( executionContext ) ;
var defaultAccessToken = executionContext . GetGitHubContext ( "token" ) ;
var configurationStore = HostContext . GetService < IConfigurationStore > ( ) ;
var runnerSettings = configurationStore . GetSettings ( ) ;
2020-06-02 17:21:50 -04:00
2020-06-09 08:53:28 -04:00
foreach ( var actionDownloadInfo in actionDownloadInfos . Actions . Values )
{
// 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 ) ) ;
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 ) ;
2021-08-02 12:59:09 -07:00
executionContext . Output ( $"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}' (SHA:{downloadInfo.ResolvedSha})" ) ;
2020-06-02 17:21:50 -04:00
}
2021-07-01 13:34:28 -04:00
await DownloadRepositoryActionAsync ( executionContext , downloadInfo , destDirectory ) ;
2020-06-02 17:21:50 -04:00
}
2020-04-23 17:02:13 -04:00
private string GetApiUrl ( IExecutionContext executionContext )
{
string apiUrl = executionContext . GetGitHubContext ( "api_url" ) ;
if ( ! string . IsNullOrEmpty ( apiUrl ) )
{
return apiUrl ;
}
// Once the api_url is set for hosted, we can remove this fallback (it doesn't make sense for GHES)
2020-05-11 12:23:02 -04:00
return _dotcomApiUrl ;
2020-04-23 17:02:13 -04:00
}
private static string BuildLinkToActionArchive ( string apiUrl , string repository , string @ref )
{
2019-10-10 00:52:42 -04:00
#if OS_WINDOWS
2020-04-23 17:02:13 -04:00
return $"{apiUrl}/repos/{repository}/zipball/{@ref}" ;
2019-10-10 00:52:42 -04:00
#else
2020-04-23 17:02:13 -04:00
return $"{apiUrl}/repos/{repository}/tarball/{@ref}" ;
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
2021-07-01 13:34:28 -04:00
private async Task DownloadRepositoryActionAsync ( IExecutionContext executionContext , WebApi . ActionDownloadInfo downloadInfo , string destDirectory )
2020-04-23 17:02:13 -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
Trace . Info ( $"Save archive '{link}' into {archiveFile}." ) ;
2019-10-10 00:52:42 -04:00
try
{
int retryCount = 0 ;
2020-01-20 18:22:59 +01:00
// Allow up to 20 * 60s for any action to be downloaded from github graph.
2019-10-10 00:52:42 -04:00
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 ) )
{
2021-07-01 13:34:28 -04:00
httpClient . DefaultRequestHeaders . Authorization = CreateAuthHeader ( downloadInfo . Authentication ? . Token ) ;
2019-10-10 00:52:42 -04:00
2020-04-09 14:33:16 -04:00
httpClient . DefaultRequestHeaders . UserAgent . AddRange ( HostContext . UserAgents ) ;
2020-04-23 17:02:13 -04:00
using ( var response = await httpClient . GetAsync ( link ) )
2019-10-10 00:52:42 -04:00
{
2020-04-23 17:02:13 -04:00
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 ( link ) ) ;
}
else
{
// Something else bad happened, let's go to our retry logic
response . EnsureSuccessStatusCode ( ) ;
}
2019-10-10 00:52:42 -04:00
}
}
}
catch ( OperationCanceledException ) when ( executionContext . CancellationToken . IsCancellationRequested )
{
2020-04-23 17:02:13 -04:00
Trace . Info ( "Action download has been cancelled." ) ;
throw ;
}
catch ( ActionNotFoundException )
{
Trace . Info ( $"The action at '{link}' does not exist" ) ;
2019-10-10 00:52:42 -04:00
throw ;
}
catch ( Exception ex ) when ( retryCount < 2 )
{
retryCount + + ;
2020-04-23 17:02:13 -04:00
Trace . Error ( $"Fail to download archive '{link}' -- Attempt: {retryCount}" ) ;
2019-10-10 00:52:42 -04:00
Trace . Error ( ex ) ;
if ( actionDownloadTimeout . Token . IsCancellationRequested )
{
// action download didn't finish within timeout
2020-04-23 17:02:13 -04:00
executionContext . Warning ( $"Action '{link}' didn't finish download within {timeoutSeconds} seconds." ) ;
2019-10-10 00:52:42 -04:00
}
else
{
2020-04-23 17:02:13 -04:00
executionContext . Warning ( $"Failed to download action '{link}'. Error: {ex.Message}" ) ;
2019-10-10 00:52:42 -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 ) ) ;
2020-04-23 17:02:13 -04:00
executionContext . Debug ( $"Download '{link}' to '{archiveFile}'" ) ;
2019-10-10 00:52:42 -04:00
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." ) ;
2020-04-23 17:02:13 -04:00
string watermarkFile = GetWatermarkFilePath ( destDirectory ) ;
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-05-11 12:23:02 -04:00
private void ConfigureAuthorizationFromContext ( IExecutionContext executionContext , HttpClient httpClient )
{
var authToken = Environment . GetEnvironmentVariable ( "_GITHUB_ACTION_TOKEN" ) ;
if ( string . IsNullOrEmpty ( authToken ) )
{
// TODO: Deprecate the PREVIEW_ACTION_TOKEN
2020-07-19 19:05:47 -04:00
authToken = executionContext . Global . Variables . Get ( "PREVIEW_ACTION_TOKEN" ) ;
2020-05-11 12:23:02 -04:00
}
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 ) ;
}
}
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 ;
if ( containerAction . Image . EndsWith ( "Dockerfile" ) | | containerAction . Image . EndsWith ( "dockerfile" ) )
{
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 ) ;
}
}
// TODO: remove once we remove the DistributedTask.EnableCompositeActions FF
2021-07-01 13:34:28 -04:00
foreach ( var step in compositeAction . Steps )
{
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 ;
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
{
var fullPath = IOUtil . ResolvePath ( actionEntryDirectory , "." ) ; // resolve full path without access filesystem.
2020-01-20 18:22:59 +01:00
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?" ) ;
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 static string GetDownloadInfoLookupKey ( WebApi . ActionDownloadInfo info )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
ArgUtil . NotNullOrEmpty ( info . NameWithOwner , nameof ( info . NameWithOwner ) ) ;
ArgUtil . NotNullOrEmpty ( info . Ref , nameof ( info . Ref ) ) ;
return $"{info.NameWithOwner}@{info.Ref}" ;
}
2020-06-02 17:21:50 -04:00
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
{
2020-06-09 08:53:28 -04:00
return null ;
2020-06-02 17:21:50 -04:00
}
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
}
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 ; }
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
}