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 ; }
2020-04-13 21:46:30 -04:00
Task < PrepareResult > PrepareActionsAsync ( IExecutionContext executionContext , IEnumerable < Pipelines . JobStep > steps ) ;
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
private readonly Dictionary < Guid , ContainerInfo > _cachedActionContainers = new Dictionary < Guid , ContainerInfo > ( ) ;
public Dictionary < Guid , ContainerInfo > CachedActionContainers = > _cachedActionContainers ;
2020-04-13 21:46:30 -04:00
public async Task < PrepareResult > PrepareActionsAsync ( IExecutionContext executionContext , IEnumerable < Pipelines . JobStep > steps )
2019-10-10 00:52:42 -04:00
{
ArgUtil . NotNull ( executionContext , nameof ( executionContext ) ) ;
ArgUtil . NotNull ( steps , nameof ( steps ) ) ;
executionContext . Output ( "Prepare all required actions" ) ;
Dictionary < string , List < Guid > > imagesToPull = new Dictionary < string , List < Guid > > ( StringComparer . OrdinalIgnoreCase ) ;
Dictionary < string , List < Guid > > imagesToBuild = new Dictionary < string , List < Guid > > ( StringComparer . OrdinalIgnoreCase ) ;
Dictionary < string , ActionContainer > imagesToBuildInfo = new Dictionary < string , ActionContainer > ( StringComparer . OrdinalIgnoreCase ) ;
List < JobExtensionRunner > containerSetupSteps = new List < JobExtensionRunner > ( ) ;
2020-04-13 21:46:30 -04:00
Dictionary < Guid , IActionRunner > preStepTracker = new Dictionary < Guid , IActionRunner > ( ) ;
2019-10-10 00:52:42 -04:00
IEnumerable < Pipelines . ActionStep > actions = steps . OfType < Pipelines . ActionStep > ( ) ;
2020-03-30 07:45:06 -04:00
// TODO: Deprecate the PREVIEW_ACTION_TOKEN
2019-10-10 00:52:42 -04:00
// Log even if we aren't using it to ensure users know.
if ( ! string . IsNullOrEmpty ( executionContext . Variables . Get ( "PREVIEW_ACTION_TOKEN" ) ) )
{
2020-03-30 07:45:06 -04:00
executionContext . Warning ( "The 'PREVIEW_ACTION_TOKEN' secret is deprecated. Please remove it from the repository's secrets" ) ;
2019-10-10 00:52:42 -04:00
}
2020-03-24 21:51:37 -04:00
// Clear the cache (for self-hosted runners)
2020-04-23 17:02:13 -04:00
IOUtil . DeleteDirectory ( HostContext . GetDirectory ( WellKnownDirectory . Actions ) , executionContext . CancellationToken ) ;
2019-10-28 11:56:12 -04:00
2020-06-02 17:21:50 -04:00
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
var newActionMetadata = executionContext . Variables . GetBoolean ( "DistributedTask.NewActionMetadata" ) ? ? false ;
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 ) ) ;
if ( ! imagesToPull . ContainsKey ( containerReference . Image ) )
{
imagesToPull [ containerReference . Image ] = new List < Guid > ( ) ;
}
Trace . Info ( $"Action {action.Name} ({action.Id}) needs to pull image '{containerReference.Image}'" ) ;
imagesToPull [ containerReference . Image ] . Add ( action . Id ) ;
}
2020-06-02 17:21:50 -04:00
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
else if ( action . Reference . Type = = Pipelines . ActionSourceType . Repository & & ! newActionMetadata )
2019-10-10 00:52:42 -04:00
{
// only download the repository archive
await DownloadRepositoryActionAsync ( executionContext , action ) ;
// more preparation base on content in the repository (action.yml)
var setupInfo = PrepareRepositoryActionAsync ( executionContext , action ) ;
if ( setupInfo ! = null )
{
if ( ! string . IsNullOrEmpty ( setupInfo . Image ) )
{
if ( ! imagesToPull . ContainsKey ( setupInfo . Image ) )
{
imagesToPull [ setupInfo . Image ] = new List < Guid > ( ) ;
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to pull image '{setupInfo.Image}'" ) ;
imagesToPull [ setupInfo . Image ] . Add ( action . Id ) ;
}
else
{
ArgUtil . NotNullOrEmpty ( setupInfo . ActionRepository , nameof ( setupInfo . ActionRepository ) ) ;
if ( ! imagesToBuild . ContainsKey ( setupInfo . ActionRepository ) )
{
imagesToBuild [ setupInfo . ActionRepository ] = new List < Guid > ( ) ;
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to build image '{setupInfo.Dockerfile}'" ) ;
imagesToBuild [ setupInfo . ActionRepository ] . Add ( action . Id ) ;
imagesToBuildInfo [ setupInfo . ActionRepository ] = setupInfo ;
}
}
2020-04-13 21:46:30 -04:00
2020-06-02 17:21:50 -04:00
var repoAction = action . Reference as Pipelines . RepositoryPathReference ;
if ( repoAction . RepositoryType ! = Pipelines . PipelineConstants . SelfAlias )
{
var definition = LoadAction ( executionContext , action ) ;
if ( definition . Data . Execution . HasPre )
{
var actionRunner = HostContext . CreateService < IActionRunner > ( ) ;
actionRunner . Action = action ;
actionRunner . Stage = ActionRunStage . Pre ;
actionRunner . Condition = definition . Data . Execution . InitCondition ;
Trace . Info ( $"Add 'pre' execution for {action.Id}" ) ;
preStepTracker [ action . Id ] = actionRunner ;
}
}
}
else if ( action . Reference . Type = = Pipelines . ActionSourceType . Repository & & newActionMetadata )
{
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 ) ;
if ( setupInfo ! = null )
{
if ( ! string . IsNullOrEmpty ( setupInfo . Image ) )
{
if ( ! imagesToPull . ContainsKey ( setupInfo . Image ) )
{
imagesToPull [ setupInfo . Image ] = new List < Guid > ( ) ;
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to pull image '{setupInfo.Image}'" ) ;
imagesToPull [ setupInfo . Image ] . Add ( action . Id ) ;
}
else
{
ArgUtil . NotNullOrEmpty ( setupInfo . ActionRepository , nameof ( setupInfo . ActionRepository ) ) ;
if ( ! imagesToBuild . ContainsKey ( setupInfo . ActionRepository ) )
{
imagesToBuild [ setupInfo . ActionRepository ] = new List < Guid > ( ) ;
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to build image '{setupInfo.Dockerfile}'" ) ;
imagesToBuild [ setupInfo . ActionRepository ] . Add ( action . Id ) ;
imagesToBuildInfo [ setupInfo . ActionRepository ] = setupInfo ;
}
}
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 ) ;
if ( definition . Data . Execution . HasPre )
{
var actionRunner = HostContext . CreateService < IActionRunner > ( ) ;
actionRunner . Action = action ;
actionRunner . Stage = ActionRunStage . Pre ;
actionRunner . Condition = definition . Data . Execution . InitCondition ;
Trace . Info ( $"Add 'pre' execution for {action.Id}" ) ;
preStepTracker [ action . Id ] = actionRunner ;
}
}
2019-10-10 00:52:42 -04:00
}
}
if ( imagesToPull . Count > 0 )
{
foreach ( var imageToPull in imagesToPull )
{
Trace . Info ( $"{imageToPull.Value.Count} steps need to pull image '{imageToPull.Key}'" ) ;
containerSetupSteps . Add ( new JobExtensionRunner ( runAsync : this . PullActionContainerAsync ,
condition : $"{PipelineTemplateConstants.Success}()" ,
displayName : $"Pull {imageToPull.Key}" ,
data : new ContainerSetupInfo ( imageToPull . Value , imageToPull . Key ) ) ) ;
}
}
if ( imagesToBuild . Count > 0 )
{
foreach ( var imageToBuild in imagesToBuild )
{
var setupInfo = imagesToBuildInfo [ imageToBuild . Key ] ;
Trace . Info ( $"{imageToBuild.Value.Count} steps need to build image from '{setupInfo.Dockerfile}'" ) ;
containerSetupSteps . Add ( new JobExtensionRunner ( runAsync : this . BuildActionContainerAsync ,
condition : $"{PipelineTemplateConstants.Success}()" ,
displayName : $"Build {setupInfo.ActionRepository}" ,
data : new ContainerSetupInfo ( imageToBuild . Value , setupInfo . Dockerfile , setupInfo . WorkingDirectory ) ) ) ;
}
}
#if ! OS_LINUX
if ( containerSetupSteps . Count > 0 )
{
executionContext . Output ( "Container action is only supported on Linux, skip pull and build docker images." ) ;
containerSetupSteps . Clear ( ) ;
}
#endif
2020-04-13 21:46:30 -04:00
return new PrepareResult ( containerSetupSteps , preStepTracker ) ;
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-06-10 17:50:14 -04:00
else if ( definition . Data . Execution . ExecutionType = = ActionExecutionType . Composite ) {
// Don't do anything for now
}
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 ) ) ;
executionContext . Output ( $"Pull down action image '{setupInfo.Container.Image}'" ) ;
// Pull down docker image with retry up to 3 times
var dockerManger = HostContext . GetService < IDockerCommandManager > ( ) ;
int retryCount = 0 ;
int pullExitCode = 0 ;
while ( retryCount < 3 )
{
pullExitCode = await dockerManger . DockerPull ( executionContext , setupInfo . Container . Image ) ;
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 ) ;
}
}
}
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 ) ) ;
executionContext . Output ( $"Build container for action use: '{setupInfo.Container.Dockerfile}'." ) ;
// Build docker image with retry up to 3 times
var dockerManger = HostContext . GetService < IDockerCommandManager > ( ) ;
int retryCount = 0 ;
int buildExitCode = 0 ;
var imageName = $"{dockerManger.DockerInstanceLabel}:{Guid.NewGuid().ToString(" N ")}" ;
while ( retryCount < 3 )
{
2020-05-11 13:57:31 -04:00
buildExitCode = await dockerManger . DockerBuild (
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 ) ;
}
}
}
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 removed when we switch to a REST API call to the service to resolve the download info
2020-06-09 08:53:28 -04:00
private async Task < bool > RepoExistsAsync ( IExecutionContext executionContext , WebApi . ActionDownloadInfo actionDownloadInfo , string token )
2020-06-02 17:21:50 -04:00
{
var apiUrl = GetApiUrl ( executionContext ) ;
2020-06-09 08:53:28 -04:00
var repoUrl = $"{apiUrl}/repos/{actionDownloadInfo.NameWithOwner}" ;
2020-06-02 17:21:50 -04:00
for ( var attempt = 1 ; attempt < = 3 ; attempt + + )
{
executionContext . Debug ( $"Checking whether repo exists: {repoUrl}" ) ;
try
{
using ( var httpClientHandler = HostContext . CreateHttpClientHandler ( ) )
using ( var httpClient = new HttpClient ( httpClientHandler ) )
{
2020-06-09 08:53:28 -04:00
httpClient . DefaultRequestHeaders . Authorization = CreateAuthHeader ( token ) ;
2020-06-02 17:21:50 -04:00
httpClient . DefaultRequestHeaders . UserAgent . AddRange ( HostContext . UserAgents ) ;
using ( var response = await httpClient . GetAsync ( repoUrl ) )
{
if ( response . IsSuccessStatusCode )
{
return true ;
}
else if ( response . StatusCode = = HttpStatusCode . NotFound )
{
return false ;
}
else
{
// Throw
response . EnsureSuccessStatusCode ( ) ;
}
}
}
}
catch ( Exception ex )
{
if ( attempt < 3 )
{
2020-06-09 08:53:28 -04:00
executionContext . Debug ( $"Failed checking whether repo '{actionDownloadInfo.NameWithOwner}' exists: {ex.Message}" ) ;
2020-06-02 17:21:50 -04:00
}
else
{
2020-06-09 08:53:28 -04:00
executionContext . Error ( $"Failed checking whether repo '{actionDownloadInfo.NameWithOwner}' exists: {ex.Message}" ) ;
2020-06-02 17:21:50 -04:00
throw ;
}
}
}
return false ; // Never reaches here
}
// 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 ,
} ;
} )
. 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-06-09 08:53:28 -04:00
actionDownloadInfos = await jobServer . ResolveActionDownloadInfoAsync ( executionContext . Plan . ScopeIdentifier , executionContext . Plan . PlanType , executionContext . Plan . PlanId , new WebApi . ActionReferenceList { Actions = actionReferences } , executionContext . CancellationToken ) ;
break ;
2020-06-02 17:21:50 -04:00
}
2020-06-09 08:53:28 -04:00
catch ( Exception ex ) when ( attempt < 3 )
{
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 ) ;
}
}
}
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-09 08:53:28 -04:00
// Temporary code: Fix token and download URL
2020-06-02 17:21:50 -04:00
if ( runnerSettings . IsHostedServer )
{
2020-06-09 08:53:28 -04:00
actionDownloadInfo . Authentication = new WebApi . ActionDownloadAuthentication { Token = defaultAccessToken } ;
actionDownloadInfo . TarballUrl = actionDownloadInfo . TarballUrl . Replace ( "<GITHUB_API_URL>" , apiUrl ) ;
actionDownloadInfo . ZipballUrl = actionDownloadInfo . ZipballUrl . Replace ( "<GITHUB_API_URL>" , apiUrl ) ;
2020-06-02 17:21:50 -04:00
}
2020-06-09 08:53:28 -04:00
else if ( await RepoExistsAsync ( executionContext , actionDownloadInfo , defaultAccessToken ) )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
actionDownloadInfo . Authentication = new WebApi . ActionDownloadAuthentication { Token = defaultAccessToken } ;
actionDownloadInfo . TarballUrl = actionDownloadInfo . TarballUrl . Replace ( "<GITHUB_API_URL>" , apiUrl ) ;
actionDownloadInfo . ZipballUrl = actionDownloadInfo . ZipballUrl . Replace ( "<GITHUB_API_URL>" , apiUrl ) ;
2020-06-02 17:21:50 -04:00
}
else
{
2020-06-09 08:53:28 -04:00
actionDownloadInfo . TarballUrl = actionDownloadInfo . TarballUrl . Replace ( "<GITHUB_API_URL>" , "https://api.github.com" ) ;
actionDownloadInfo . ZipballUrl = actionDownloadInfo . ZipballUrl . Replace ( "<GITHUB_API_URL>" , "https://api.github.com" ) ;
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
}
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
2019-10-10 00:52:42 -04:00
private async Task DownloadRepositoryActionAsync ( IExecutionContext executionContext , Pipelines . ActionStep repositoryAction )
{
Trace . Entering ( ) ;
ArgUtil . NotNull ( executionContext , nameof ( executionContext ) ) ;
var repositoryReference = repositoryAction . Reference as Pipelines . RepositoryPathReference ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ) ) ;
if ( string . Equals ( repositoryReference . RepositoryType , Pipelines . PipelineConstants . SelfAlias , StringComparison . OrdinalIgnoreCase ) )
{
Trace . Info ( $"Repository action is in 'self' repository." ) ;
return ;
}
if ( ! string . Equals ( repositoryReference . RepositoryType , Pipelines . RepositoryTypes . GitHub , StringComparison . OrdinalIgnoreCase ) )
{
throw new NotSupportedException ( repositoryReference . RepositoryType ) ;
}
ArgUtil . NotNullOrEmpty ( repositoryReference . Name , nameof ( repositoryReference . Name ) ) ;
ArgUtil . NotNullOrEmpty ( repositoryReference . Ref , nameof ( repositoryReference . Ref ) ) ;
string destDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ) , repositoryReference . Name . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ) , repositoryReference . Ref ) ;
2020-04-23 17:02:13 -04:00
string watermarkFile = GetWatermarkFilePath ( destDirectory ) ;
2020-03-24 21:51:37 -04:00
if ( File . Exists ( watermarkFile ) )
2019-10-10 00:52:42 -04:00
{
executionContext . Debug ( $"Action '{repositoryReference.Name}@{repositoryReference.Ref}' already downloaded at '{destDirectory}'." ) ;
return ;
}
else
{
2019-10-28 11:56:12 -04:00
// make sure we get a clean folder ready to use.
2019-10-10 00:52:42 -04:00
IOUtil . DeleteDirectory ( destDirectory , executionContext . CancellationToken ) ;
Directory . CreateDirectory ( destDirectory ) ;
executionContext . Output ( $"Download action repository '{repositoryReference.Name}@{repositoryReference.Ref}'" ) ;
}
2020-04-23 17:02:13 -04:00
var configurationStore = HostContext . GetService < IConfigurationStore > ( ) ;
var isHostedServer = configurationStore . GetSettings ( ) . IsHostedServer ;
if ( isHostedServer )
{
string apiUrl = GetApiUrl ( executionContext ) ;
string archiveLink = BuildLinkToActionArchive ( apiUrl , repositoryReference . Name , repositoryReference . Ref ) ;
2020-05-11 12:23:02 -04:00
var downloadDetails = new ActionDownloadDetails ( archiveLink , ConfigureAuthorizationFromContext ) ;
2020-06-02 17:21:50 -04:00
await DownloadRepositoryActionAsync ( executionContext , downloadDetails , null , destDirectory ) ;
2020-04-23 17:02:13 -04:00
return ;
}
else
{
string apiUrl = GetApiUrl ( executionContext ) ;
// URLs to try:
2020-05-11 12:23:02 -04:00
var downloadAttempts = new List < ActionDownloadDetails > {
2020-04-23 17:02:13 -04:00
// A built-in action or an action the user has created, on their GHES instance
// Example: https://my-ghes/api/v3/repos/my-org/my-action/tarball/v1
2020-05-11 12:23:02 -04:00
new ActionDownloadDetails (
BuildLinkToActionArchive ( apiUrl , repositoryReference . Name , repositoryReference . Ref ) ,
ConfigureAuthorizationFromContext ) ,
// The same action, on GitHub.com
// Example: https://api.github.com/repos/my-org/my-action/tarball/v1
new ActionDownloadDetails (
BuildLinkToActionArchive ( _dotcomApiUrl , repositoryReference . Name , repositoryReference . Ref ) ,
configureAuthorization : ( e , h ) = > { /* no authorization for dotcom */ } )
2020-04-23 17:02:13 -04:00
} ;
2020-05-11 12:23:02 -04:00
foreach ( var downloadAttempt in downloadAttempts )
2020-04-23 17:02:13 -04:00
{
try
{
2020-06-02 17:21:50 -04:00
await DownloadRepositoryActionAsync ( executionContext , downloadAttempt , null , destDirectory ) ;
2020-04-23 17:02:13 -04:00
return ;
}
catch ( ActionNotFoundException )
{
2020-05-11 12:23:02 -04:00
Trace . Info ( $"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}' at {downloadAttempt.ArchiveLink}" ) ;
2020-04-23 17:02:13 -04:00
continue ;
}
}
2020-05-11 12:23:02 -04:00
throw new ActionNotFoundException ( $"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}'. Paths attempted: {string.Join(" , ", downloadAttempts.Select(d => d.ArchiveLink))}" ) ;
2020-04-23 17:02:13 -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 ) ;
executionContext . Output ( $"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'" ) ;
}
await DownloadRepositoryActionAsync ( executionContext , null , downloadInfo , destDirectory ) ;
}
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
2020-06-02 17:21:50 -04:00
// todo: Remove the parameter "actionDownloadDetails" when feature flag DistributedTask.NewActionMetadata is removed
2020-06-09 08:53:28 -04:00
private async Task DownloadRepositoryActionAsync ( IExecutionContext executionContext , ActionDownloadDetails actionDownloadDetails , 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" ) ;
2020-06-09 08:53:28 -04:00
string link = downloadInfo ? . ZipballUrl ? ? actionDownloadDetails . ArchiveLink ;
2019-10-10 00:52:42 -04:00
#else
string archiveFile = Path . Combine ( tempDirectory , $"{Guid.NewGuid()}.tar.gz" ) ;
2020-06-09 08:53:28 -04:00
string link = downloadInfo ? . TarballUrl ? ? actionDownloadDetails . ArchiveLink ;
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 ) )
{
2020-06-02 17:21:50 -04:00
// Legacy
if ( downloadInfo = = null )
{
actionDownloadDetails . ConfigureAuthorization ( executionContext , httpClient ) ;
}
// FF DistributedTask.NewActionMetadata
else
{
2020-06-09 08:53:28 -04:00
httpClient . DefaultRequestHeaders . Authorization = CreateAuthHeader ( downloadInfo . Authentication ? . Token ) ;
2020-06-02 17:21:50 -04:00
}
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-06-02 17:21:50 -04:00
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
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
authToken = executionContext . Variables . Get ( "PREVIEW_ACTION_TOKEN" ) ;
}
if ( ! string . IsNullOrEmpty ( authToken ) )
{
HostContext . SecretMasker . AddValue ( authToken ) ;
var base64EncodingToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"PAT:{authToken}" ) ) ;
httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Basic" , base64EncodingToken ) ;
}
else
{
var accessToken = executionContext . GetGitHubContext ( "token" ) ;
var base64EncodingToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"x-access-token:{accessToken}" ) ) ;
httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Basic" , base64EncodingToken ) ;
}
}
2020-04-23 17:02:13 -04:00
private string GetWatermarkFilePath ( string directory ) = > directory + ".completed" ;
2019-10-10 00:52:42 -04:00
private ActionContainer PrepareRepositoryActionAsync ( IExecutionContext executionContext , Pipelines . ActionStep repositoryAction )
{
var repositoryReference = repositoryAction . Reference as Pipelines . RepositoryPathReference ;
if ( string . Equals ( repositoryReference . RepositoryType , Pipelines . PipelineConstants . SelfAlias , StringComparison . OrdinalIgnoreCase ) )
{
Trace . Info ( $"Repository action is in 'self' repository." ) ;
return null ;
}
var setupInfo = new ActionContainer ( ) ;
string destDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ) , repositoryReference . Name . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ) , repositoryReference . Ref ) ;
string actionEntryDirectory = destDirectory ;
string dockerFileRelativePath = repositoryReference . Name ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ) ) ;
if ( ! string . IsNullOrEmpty ( repositoryReference . Path ) )
{
actionEntryDirectory = Path . Combine ( destDirectory , repositoryReference . Path ) ;
dockerFileRelativePath = $"{dockerFileRelativePath}/{repositoryReference.Path}" ;
setupInfo . ActionRepository = $"{repositoryReference.Name}/{repositoryReference.Path}@{repositoryReference.Ref}" ;
}
else
{
setupInfo . ActionRepository = $"{repositoryReference.Name}@{repositoryReference.Ref}" ;
}
// find the docker file or action.yml file
var dockerFile = Path . Combine ( actionEntryDirectory , "Dockerfile" ) ;
var dockerFileLowerCase = Path . Combine ( actionEntryDirectory , "dockerfile" ) ;
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}'." ) ;
setupInfo . Dockerfile = dockerFileFullPath ;
setupInfo . WorkingDirectory = destDirectory ;
return setupInfo ;
}
else if ( containerAction . Image . StartsWith ( "docker://" , StringComparison . OrdinalIgnoreCase ) )
{
var actionImage = containerAction . Image . Substring ( "docker://" . Length ) ;
executionContext . Debug ( $"Container image for action: '{actionImage}'." ) ;
setupInfo . Image = actionImage ;
return setupInfo ;
}
else
{
throw new NotSupportedException ( $"'{containerAction.Image}' should be either '[path]/Dockerfile' or 'docker://image[:tag]'." ) ;
}
}
else if ( actionDefinitionData . Execution . ExecutionType = = ActionExecutionType . NodeJS )
{
Trace . Info ( $"Action node.js file: {(actionDefinitionData.Execution as NodeJSActionExecutionData).Script}, no more preparation." ) ;
return null ;
}
else if ( actionDefinitionData . Execution . ExecutionType = = ActionExecutionType . Plugin )
{
Trace . Info ( $"Action plugin: {(actionDefinitionData.Execution as PluginActionExecutionData).Plugin}, no more preparation." ) ;
return null ;
}
2020-06-10 17:50:14 -04:00
else if ( actionDefinitionData . Execution . ExecutionType = = ActionExecutionType . Composite )
{
// Trace.Info($"Action composite: {(actionDefinitionData.Execution as CompositeActionExecutionData).Unknown}, no more preparation.");
return null ;
}
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}'." ) ;
setupInfo . Dockerfile = dockerFile ;
setupInfo . WorkingDirectory = destDirectory ;
return setupInfo ;
}
else if ( File . Exists ( dockerFileLowerCase ) )
{
executionContext . Debug ( $"Dockerfile for action: '{dockerFileLowerCase}'." ) ;
setupInfo . Dockerfile = dockerFileLowerCase ;
setupInfo . WorkingDirectory = destDirectory ;
return setupInfo ;
}
else
{
var fullPath = IOUtil . ResolvePath ( actionEntryDirectory , "." ) ; // resolve full path without access filesystem.
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
}
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
2020-05-11 12:23:02 -04:00
private class ActionDownloadDetails
{
public string ArchiveLink { get ; }
public Action < IExecutionContext , HttpClient > ConfigureAuthorization { get ; }
public ActionDownloadDetails ( string archiveLink , Action < IExecutionContext , HttpClient > configureAuthorization )
{
ArchiveLink = archiveLink ;
ConfigureAuthorization = configureAuthorization ;
}
}
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-10 17:50:14 -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-10 17:50:14 -04:00
public sealed class CompositeActionExecutionData : ActionExecutionData
{
// Do I actually need this??
// Pre => pre execution for checking steps before, ibid for post
// Look at Script for dummy example
public override ActionExecutionType ExecutionType = > ActionExecutionType . Composite ;
public override bool HasPre = > false ;
public override bool HasPost = > false ;
2020-06-11 17:55:13 -04:00
public List < Pipelines . ActionStep > Steps { get ; set ; }
2020-06-10 17:50:14 -04:00
// public string Script { get; set; }
// public string Pre { get; set; }
// public string Post { get; set; }
}
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 ; }
}
}