2019-10-10 00:52:42 -04:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
2020-03-18 12:08:51 -04:00
using System.Text ;
2019-10-10 00:52:42 -04:00
using System.Threading ;
using System.Threading.Tasks ;
2020-03-18 12:08:51 -04:00
using GitHub.DistributedTask.Expressions2 ;
2022-04-01 15:18:53 +02:00
using GitHub.DistributedTask.ObjectTemplating.Tokens ;
2019-10-10 00:52:42 -04:00
using GitHub.DistributedTask.Pipelines.ContextData ;
2020-03-03 22:38:19 -05:00
using GitHub.DistributedTask.Pipelines.ObjectTemplating ;
2019-10-10 00:52:42 -04:00
using GitHub.DistributedTask.WebApi ;
using GitHub.Runner.Common ;
2022-02-11 16:18:41 -05:00
using GitHub.Runner.Common.Util ;
2019-10-10 00:52:42 -04:00
using GitHub.Runner.Sdk ;
2020-03-18 12:08:51 -04:00
using GitHub.Runner.Worker.Container ;
2022-04-11 14:43:24 +02:00
using GitHub.Runner.Worker.Handlers ;
2019-10-10 00:52:42 -04:00
using Newtonsoft.Json ;
2019-10-25 10:38:56 -04:00
using ObjectTemplating = GitHub . DistributedTask . ObjectTemplating ;
using Pipelines = GitHub . DistributedTask . Pipelines ;
2019-10-10 00:52:42 -04:00
namespace GitHub.Runner.Worker
{
public class ExecutionContextType
{
public static string Job = "Job" ;
public static string Task = "Task" ;
}
[ServiceLocator(Default = typeof(ExecutionContext))]
public interface IExecutionContext : IRunnerService
{
Guid Id { get ; }
2021-07-28 15:35:21 -04:00
Guid EmbeddedId { get ; }
2019-10-10 00:52:42 -04:00
string ScopeName { get ; }
2021-08-04 11:39:22 -04:00
string SiblingScopeName { get ; }
2019-10-10 00:52:42 -04:00
string ContextName { get ; }
2021-10-29 15:45:42 +02:00
ActionRunStage Stage { get ; }
2019-10-10 00:52:42 -04:00
Task ForceCompleted { get ; }
TaskResult ? Result { get ; set ; }
2020-03-17 21:18:42 -04:00
TaskResult ? Outcome { get ; set ; }
2019-10-10 00:52:42 -04:00
string ResultCode { get ; set ; }
TaskResult ? CommandResult { get ; set ; }
CancellationToken CancellationToken { get ; }
2020-07-19 19:05:47 -04:00
GlobalContext Global { get ; }
2019-10-10 00:52:42 -04:00
Dictionary < string , string > IntraActionState { get ; }
2020-03-14 17:54:58 -04:00
Dictionary < string , VariableValue > JobOutputs { get ; }
2020-10-21 12:14:21 -04:00
ActionsEnvironmentReference ActionsEnvironment { get ; }
2022-02-11 16:18:41 -05:00
ActionsStepTelemetry StepTelemetry { get ; }
2019-10-10 00:52:42 -04:00
DictionaryContextData ExpressionValues { get ; }
2020-03-18 12:08:51 -04:00
IList < IFunctionInfo > ExpressionFunctions { get ; }
2019-10-10 00:52:42 -04:00
JobContext JobContext { get ; }
// Only job level ExecutionContext has JobSteps
2020-07-29 16:19:04 -04:00
Queue < IStep > JobSteps { get ; }
2019-10-10 00:52:42 -04:00
// Only job level ExecutionContext has PostJobSteps
Stack < IStep > PostJobSteps { get ; }
2021-10-27 09:31:58 -04:00
Dictionary < Guid , string > EmbeddedStepsWithPostRegistered { get ; }
2021-07-28 15:35:21 -04:00
// Keep track of embedded steps states
Dictionary < Guid , Dictionary < string , string >> EmbeddedIntraActionState { get ; }
2019-10-10 00:52:42 -04:00
2019-10-25 10:38:56 -04:00
bool EchoOnActionCommand { get ; set ; }
2021-04-30 15:48:53 -05:00
bool IsEmbedded { get ; }
2020-07-23 09:45:00 -04:00
2022-06-29 02:50:50 +02:00
List < string > StepEnvironmentOverrides { get ; }
2020-07-22 18:01:50 -04:00
ExecutionContext Root { get ; }
2019-10-10 00:52:42 -04:00
// Initialize
void InitializeJob ( Pipelines . AgentJobRequestMessage message , CancellationToken token );
void CancelToken ();
2021-10-29 15:45:42 +02:00
IExecutionContext CreateChild ( Guid recordId , string displayName , string refName , string scopeName , string contextName , ActionRunStage stage , Dictionary < string , string > intraActionState = null , int? recordOrder = null , IPagingLogger logger = null , bool isEmbedded = false , CancellationTokenSource cancellationTokenSource = null , Guid embeddedId = default ( Guid ), string siblingScopeName = null );
IExecutionContext CreateEmbeddedChild ( string scopeName , string contextName , Guid embeddedId , ActionRunStage stage , Dictionary < string , string > intraActionState = null , string siblingScopeName = null );
2019-10-10 00:52:42 -04:00
// logging
long Write ( string tag , string message );
void QueueAttachFile ( string type , string name , string filePath );
2022-12-28 11:56:53 -05:00
void QueueSummaryFile ( string name , string filePath , Guid stepRecordId );
2019-10-10 00:52:42 -04:00
// timeline record update methods
void Start ( string currentOperation = null );
TaskResult Complete ( TaskResult ? result = null , string currentOperation = null , string resultCode = null );
void SetEnvContext ( string name , string value );
void SetRunnerContext ( string name , string value );
string GetGitHubContext ( string name );
void SetGitHubContext ( string name , string value );
void SetOutput ( string name , string value , out string reference );
void SetTimeout ( TimeSpan ? timeout );
void AddIssue ( Issue issue , string message = null );
void Progress ( int percentage , string currentOperation = null );
void UpdateDetailTimelineRecord ( TimelineRecord record );
void UpdateTimelineRecordDisplayName ( string displayName );
// matchers
void Add ( OnMatcherChanged handler );
void Remove ( OnMatcherChanged handler );
void AddMatchers ( IssueMatchersConfig matcher );
void RemoveMatchers ( IEnumerable < string > owners );
IEnumerable < IssueMatcherConfig > GetMatchers ();
// others
void ForceTaskComplete ();
2020-04-13 21:46:30 -04:00
void RegisterPostJobStep ( IStep step );
2022-02-11 16:18:41 -05:00
void PublishStepTelemetry ();
2022-04-01 15:18:53 +02:00
void ApplyContinueOnError ( TemplateToken continueOnError );
void UpdateGlobalStepsContext ();
2022-03-18 02:35:04 +01:00
void WriteWebhookPayload ();
2019-10-10 00:52:42 -04:00
}
public sealed class ExecutionContext : RunnerService , IExecutionContext
{
private const int _maxIssueCount = 10 ;
2020-05-12 16:09:13 -04:00
private const int _throttlingDelayReportThreshold = 10 * 1000 ; // Don't report throttling with less than 10 seconds delay
2022-02-16 12:18:21 -05:00
private const int _maxIssueMessageLength = 4096 ; // Don't send issue with huge message since we can't forward them from actions to check annotation.
private const int _maxIssueCountInTelemetry = 3 ; // Only send the first 3 issues to telemetry
private const int _maxIssueMessageLengthInTelemetry = 256 ; // Only send the first 256 characters of issue message to telemetry
2019-10-10 00:52:42 -04:00
2022-10-18 10:54:08 -04:00
private readonly TimelineRecord _record = new ();
private readonly Dictionary < Guid , TimelineRecord > _detailRecords = new ();
private readonly object _loggerLock = new ();
private readonly object _matchersLock = new ();
2019-10-10 00:52:42 -04:00
private event OnMatcherChanged _onMatcherChanged ;
private IssueMatcherConfig [] _matchers ;
private IPagingLogger _logger ;
private IJobServerQueue _jobServerQueue ;
private ExecutionContext _parentExecutionContext ;
private Guid _mainTimelineId ;
private Guid _detailTimelineId ;
private bool _expandedForPostJob = false ;
private int _childTimelineRecordOrder = 0 ;
private CancellationTokenSource _cancellationTokenSource ;
2022-10-18 10:54:08 -04:00
private TaskCompletionSource < int > _forceCompleted = new ();
2019-10-10 00:52:42 -04:00
private bool _throttlingReported = false ;
// only job level ExecutionContext will track throttling delay.
private long _totalThrottlingDelayInMilliseconds = 0 ;
2022-02-11 16:18:41 -05:00
private bool _stepTelemetryPublished = false ;
2019-10-10 00:52:42 -04:00
public Guid Id => _record . Id ;
2021-07-28 15:35:21 -04:00
public Guid EmbeddedId { get ; private set ; }
2019-10-10 00:52:42 -04:00
public string ScopeName { get ; private set ; }
2021-08-04 11:39:22 -04:00
public string SiblingScopeName { get ; private set ; }
2019-10-10 00:52:42 -04:00
public string ContextName { get ; private set ; }
2021-10-29 15:45:42 +02:00
public ActionRunStage Stage { get ; private set ; }
2019-10-10 00:52:42 -04:00
public Task ForceCompleted => _forceCompleted . Task ;
public CancellationToken CancellationToken => _cancellationTokenSource . Token ;
public Dictionary < string , string > IntraActionState { get ; private set ; }
2020-03-14 17:54:58 -04:00
public Dictionary < string , VariableValue > JobOutputs { get ; private set ; }
2020-10-21 12:14:21 -04:00
public ActionsEnvironmentReference ActionsEnvironment { get ; private set ; }
2022-02-11 16:18:41 -05:00
public ActionsStepTelemetry StepTelemetry { get ; } = new ActionsStepTelemetry ();
2019-10-10 00:52:42 -04:00
public DictionaryContextData ExpressionValues { get ; } = new DictionaryContextData ();
2020-03-18 12:08:51 -04:00
public IList < IFunctionInfo > ExpressionFunctions { get ; } = new List < IFunctionInfo >();
2020-07-19 19:05:47 -04:00
// Shared pointer across job-level execution context and step-level execution contexts
public GlobalContext Global { get ; private set ; }
2019-10-10 00:52:42 -04:00
// Only job level ExecutionContext has JobSteps
2020-07-29 16:19:04 -04:00
public Queue < IStep > JobSteps { get ; private set ; }
2019-10-10 00:52:42 -04:00
// Only job level ExecutionContext has PostJobSteps
public Stack < IStep > PostJobSteps { get ; private set ; }
2020-04-13 21:46:30 -04:00
// Only job level ExecutionContext has StepsWithPostRegistered
public HashSet < Guid > StepsWithPostRegistered { get ; private set ; }
2021-07-28 15:35:21 -04:00
// Only job level ExecutionContext has EmbeddedStepsWithPostRegistered
2021-10-27 09:31:58 -04:00
public Dictionary < Guid , string > EmbeddedStepsWithPostRegistered { get ; private set ; }
2021-07-28 15:35:21 -04:00
public Dictionary < Guid , Dictionary < string , string >> EmbeddedIntraActionState { get ; private set ; }
2019-10-25 10:38:56 -04:00
public bool EchoOnActionCommand { get ; set ; }
2021-04-30 15:48:53 -05:00
// An embedded execution context shares the same record ID, record name, and logger
// as its enclosing execution context.
public bool IsEmbedded { get ; private set ; }
2020-07-23 09:45:00 -04:00
2019-10-10 00:52:42 -04:00
public TaskResult ? Result
{
get
{
return _record . Result ;
}
set
{
_record . Result = value ;
}
}
2020-03-17 21:18:42 -04:00
public TaskResult ? Outcome { get ; set ; }
2019-10-10 00:52:42 -04:00
public TaskResult ? CommandResult { get ; set ; }
private string ContextType => _record . RecordType ;
public string ResultCode
{
get
{
return _record . ResultCode ;
}
set
{
_record . ResultCode = value ;
}
}
2020-07-22 18:01:50 -04:00
public ExecutionContext Root
2019-10-10 00:52:42 -04:00
{
get
{
var result = this ;
while ( result . _parentExecutionContext != null )
{
result = result . _parentExecutionContext ;
}
return result ;
}
}
public JobContext JobContext
{
get
{
return ExpressionValues [ "job" ] as JobContext ;
}
}
2022-06-29 02:50:50 +02:00
public List < string > StepEnvironmentOverrides { get ; } = new List < string >();
2019-10-10 00:52:42 -04:00
public override void Initialize ( IHostContext hostContext )
{
base . Initialize ( hostContext );
_jobServerQueue = HostContext . GetService < IJobServerQueue >();
}
public void CancelToken ()
{
try
{
_cancellationTokenSource . Cancel ();
}
catch ( ObjectDisposedException e )
{
Trace . Info ( $"Attempted to cancel a disposed token, the execution is already complete: {e.ToString()}" );
}
}
public void ForceTaskComplete ()
{
Trace . Info ( "Force finish current task in 5 sec." );
Task . Run ( async () =>
{
await Task . Delay ( TimeSpan . FromSeconds ( 5 ));
_forceCompleted ?. TrySetResult ( 1 );
});
}
2020-04-13 21:46:30 -04:00
public void RegisterPostJobStep ( IStep step )
2019-10-10 00:52:42 -04:00
{
2021-08-04 11:39:22 -04:00
string siblingScopeName = null ;
2021-07-01 13:34:28 -04:00
if ( this . IsEmbedded )
{
2021-10-27 09:31:58 -04:00
if ( step is IActionRunner actionRunner )
2021-07-28 15:35:21 -04:00
{
2021-10-27 09:31:58 -04:00
if ( Root . EmbeddedStepsWithPostRegistered . ContainsKey ( actionRunner . Action . Id ))
{
Trace . Info ( $"'post' of '{actionRunner.DisplayName}' already push to child post step stack." );
}
2022-02-11 16:18:41 -05:00
else
2021-10-27 09:31:58 -04:00
{
2022-02-11 16:18:41 -05:00
Root . EmbeddedStepsWithPostRegistered [ actionRunner . Action . Id ] = actionRunner . Condition ;
2021-10-27 09:31:58 -04:00
}
return ;
2021-07-28 15:35:21 -04:00
}
2021-10-27 09:31:58 -04:00
}
2021-07-28 15:35:21 -04:00
else if ( step is IActionRunner actionRunner && ! Root . StepsWithPostRegistered . Add ( actionRunner . Action . Id ))
2020-04-13 21:46:30 -04:00
{
Trace . Info ( $"'post' of '{actionRunner.DisplayName}' already push to post step stack." );
return ;
}
2021-08-04 11:39:22 -04:00
if ( step is IActionRunner runner )
{
siblingScopeName = runner . Action . ContextName ;
}
2020-04-13 21:46:30 -04:00
2021-08-04 11:39:22 -04:00
step . ExecutionContext = Root . CreatePostChild ( step . DisplayName , IntraActionState , siblingScopeName );
2019-11-04 13:19:21 -05:00
Root . PostJobSteps . Push ( step );
2019-10-10 00:52:42 -04:00
}
2022-02-11 16:18:41 -05:00
public IExecutionContext CreateChild (
Guid recordId ,
string displayName ,
string refName ,
string scopeName ,
string contextName ,
ActionRunStage stage ,
Dictionary < string , string > intraActionState = null ,
int? recordOrder = null ,
IPagingLogger logger = null ,
bool isEmbedded = false ,
CancellationTokenSource cancellationTokenSource = null ,
Guid embeddedId = default ( Guid ),
string siblingScopeName = null )
2019-10-10 00:52:42 -04:00
{
Trace . Entering ();
var child = new ExecutionContext ();
child . Initialize ( HostContext );
2020-07-19 19:05:47 -04:00
child . Global = Global ;
2019-10-10 00:52:42 -04:00
child . ScopeName = scopeName ;
child . ContextName = contextName ;
2021-10-29 15:45:42 +02:00
child . Stage = stage ;
2021-07-28 15:35:21 -04:00
child . EmbeddedId = embeddedId ;
2021-08-04 11:39:22 -04:00
child . SiblingScopeName = siblingScopeName ;
2019-10-10 00:52:42 -04:00
if ( intraActionState == null )
{
child . IntraActionState = new Dictionary < string , string >( StringComparer . OrdinalIgnoreCase );
}
else
{
child . IntraActionState = intraActionState ;
}
foreach ( var pair in ExpressionValues )
{
child . ExpressionValues [ pair . Key ] = pair . Value ;
}
2020-03-18 12:08:51 -04:00
foreach ( var item in ExpressionFunctions )
{
child . ExpressionFunctions . Add ( item );
}
2020-07-22 18:01:50 -04:00
child . _cancellationTokenSource = cancellationTokenSource ?? new CancellationTokenSource ();
2019-10-10 00:52:42 -04:00
child . _parentExecutionContext = this ;
2019-10-25 10:38:56 -04:00
child . EchoOnActionCommand = EchoOnActionCommand ;
2019-10-10 00:52:42 -04:00
if ( recordOrder != null )
{
child . InitializeTimelineRecord ( _mainTimelineId , recordId , _record . Id , ExecutionContextType . Task , displayName , refName , recordOrder );
}
else
{
child . InitializeTimelineRecord ( _mainTimelineId , recordId , _record . Id , ExecutionContextType . Task , displayName , refName , ++ _childTimelineRecordOrder );
}
2020-07-13 17:55:15 -04:00
if ( logger != null )
{
child . _logger = logger ;
}
else
{
child . _logger = HostContext . CreateService < IPagingLogger >();
child . _logger . Setup ( _mainTimelineId , recordId );
}
2019-10-10 00:52:42 -04:00
2021-04-30 15:48:53 -05:00
child . IsEmbedded = isEmbedded ;
2022-02-16 12:18:21 -05:00
child . StepTelemetry . StepId = recordId ;
child . StepTelemetry . Stage = stage . ToString ();
child . StepTelemetry . IsEmbedded = isEmbedded ;
2022-08-22 18:26:52 -07:00
child . StepTelemetry . StepContextName = child . GetFullyQualifiedContextName (); ;
2020-07-23 09:45:00 -04:00
2019-10-10 00:52:42 -04:00
return child ;
}
2021-04-30 15:48:53 -05:00
/// <summary>
/// An embedded execution context shares the same record ID, record name, logger,
/// and a linked cancellation token.
/// </summary>
2022-02-11 16:18:41 -05:00
public IExecutionContext CreateEmbeddedChild (
string scopeName ,
string contextName ,
Guid embeddedId ,
ActionRunStage stage ,
Dictionary < string , string > intraActionState = null ,
string siblingScopeName = null )
2021-04-30 15:48:53 -05:00
{
2021-10-29 15:45:42 +02:00
return Root . CreateChild ( _record . Id , _record . Name , _record . Id . ToString ( "N" ), scopeName , contextName , stage , logger : _logger , isEmbedded : true , cancellationTokenSource : CancellationTokenSource . CreateLinkedTokenSource ( _cancellationTokenSource . Token ), intraActionState : intraActionState , embeddedId : embeddedId , siblingScopeName : siblingScopeName );
2021-04-30 15:48:53 -05:00
}
2019-10-10 00:52:42 -04:00
public void Start ( string currentOperation = null )
{
_record . CurrentOperation = currentOperation ?? _record . CurrentOperation ;
_record . StartTime = DateTime . UtcNow ;
_record . State = TimelineRecordState . InProgress ;
_jobServerQueue . QueueTimelineRecordUpdate ( _mainTimelineId , _record );
}
public TaskResult Complete ( TaskResult ? result = null , string currentOperation = null , string resultCode = null )
{
if ( result != null )
{
Result = result ;
}
// report total delay caused by server throttling.
2020-05-12 16:09:13 -04:00
if ( _totalThrottlingDelayInMilliseconds > _throttlingDelayReportThreshold )
2019-10-10 00:52:42 -04:00
{
this . Warning ( $"The job has experienced {TimeSpan.FromMilliseconds(_totalThrottlingDelayInMilliseconds).TotalSeconds} seconds total delay caused by server throttling." );
}
_record . CurrentOperation = currentOperation ?? _record . CurrentOperation ;
_record . ResultCode = resultCode ?? _record . ResultCode ;
_record . FinishTime = DateTime . UtcNow ;
_record . PercentComplete = 100 ;
_record . Result = _record . Result ?? TaskResult . Succeeded ;
_record . State = TimelineRecordState . Completed ;
_jobServerQueue . QueueTimelineRecordUpdate ( _mainTimelineId , _record );
// complete all detail timeline records.
if ( _detailTimelineId != Guid . Empty && _detailRecords . Count > 0 )
{
foreach ( var record in _detailRecords )
{
record . Value . FinishTime = record . Value . FinishTime ?? DateTime . UtcNow ;
record . Value . PercentComplete = record . Value . PercentComplete ?? 100 ;
record . Value . Result = record . Value . Result ?? TaskResult . Succeeded ;
record . Value . State = TimelineRecordState . Completed ;
_jobServerQueue . QueueTimelineRecordUpdate ( _detailTimelineId , record . Value );
}
}
2022-02-11 16:18:41 -05:00
PublishStepTelemetry ();
2020-05-21 11:09:50 -04:00
if ( Root != this )
{
2020-10-21 12:14:21 -04:00
// only dispose TokenSource for step level ExecutionContext
2020-05-21 11:09:50 -04:00
_cancellationTokenSource ?. Dispose ();
}
2019-10-10 00:52:42 -04:00
_logger . End ();
2022-04-01 15:18:53 +02:00
UpdateGlobalStepsContext ();
return Result . Value ;
}
public void UpdateGlobalStepsContext ()
{
2021-07-28 15:35:21 -04:00
// Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name.
2020-08-04 11:12:40 -04:00
if (! string . IsNullOrEmpty ( ContextName ) && ! ContextName . StartsWith ( "__" , StringComparison . Ordinal ))
2020-03-17 21:18:42 -04:00
{
2020-07-19 19:05:47 -04:00
Global . StepsContext . SetOutcome ( ScopeName , ContextName , ( Outcome ?? Result ?? TaskResult . Succeeded ). ToActionResult ());
Global . StepsContext . SetConclusion ( ScopeName , ContextName , ( Result ?? TaskResult . Succeeded ). ToActionResult ());
2020-03-17 21:18:42 -04:00
}
2019-10-10 00:52:42 -04:00
}
public void SetRunnerContext ( string name , string value )
{
ArgUtil . NotNullOrEmpty ( name , nameof ( name ));
var runnerContext = ExpressionValues [ "runner" ] as RunnerContext ;
runnerContext [ name ] = new StringContextData ( value );
}
public void SetEnvContext ( string name , string value )
{
ArgUtil . NotNullOrEmpty ( name , nameof ( name ));
#if OS_WINDOWS
var envContext = ExpressionValues [ "env" ] as DictionaryContextData ;
envContext [ name ] = new StringContextData ( value );
#else
var envContext = ExpressionValues [ "env" ] as CaseSensitiveDictionaryContextData ;
envContext [ name ] = new StringContextData ( value );
#endif
}
public void SetGitHubContext ( string name , string value )
{
ArgUtil . NotNullOrEmpty ( name , nameof ( name ));
var githubContext = ExpressionValues [ "github" ] as GitHubContext ;
githubContext [ name ] = new StringContextData ( value );
}
public string GetGitHubContext ( string name )
{
ArgUtil . NotNullOrEmpty ( name , nameof ( name ));
var githubContext = ExpressionValues [ "github" ] as GitHubContext ;
if ( githubContext . TryGetValue ( name , out var value ))
{
if ( value is StringContextData )
{
return value as StringContextData ;
}
else
{
return value . ToJToken (). ToString ( Formatting . Indented );
}
}
else
{
return null ;
}
}
public void SetOutput ( string name , string value , out string reference )
{
ArgUtil . NotNullOrEmpty ( name , nameof ( name ));
2021-07-28 15:35:21 -04:00
// Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name.
2020-08-04 11:12:40 -04:00
if ( string . IsNullOrEmpty ( ContextName ) || ContextName . StartsWith ( "__" , StringComparison . Ordinal ))
2019-10-10 00:52:42 -04:00
{
reference = null ;
return ;
}
// todo: restrict multiline?
2020-07-19 19:05:47 -04:00
Global . StepsContext . SetOutput ( ScopeName , ContextName , name , value , out reference );
2019-10-10 00:52:42 -04:00
}
public void SetTimeout ( TimeSpan ? timeout )
{
if ( timeout != null )
{
_cancellationTokenSource . CancelAfter ( timeout . Value );
}
}
public void Progress ( int percentage , string currentOperation = null )
{
if ( percentage > 100 || percentage < 0 )
{
throw new ArgumentOutOfRangeException ( nameof ( percentage ));
}
_record . CurrentOperation = currentOperation ?? _record . CurrentOperation ;
_record . PercentComplete = Math . Max ( percentage , _record . PercentComplete . Value );
_jobServerQueue . QueueTimelineRecordUpdate ( _mainTimelineId , _record );
}
// This is not thread safe, the caller need to take lock before calling issue()
public void AddIssue ( Issue issue , string logMessage = null )
{
ArgUtil . NotNull ( issue , nameof ( issue ));
if ( string . IsNullOrEmpty ( logMessage ))
{
logMessage = issue . Message ;
}
issue . Message = HostContext . SecretMasker . MaskSecrets ( issue . Message );
2022-02-16 12:18:21 -05:00
if ( issue . Message . Length > _maxIssueMessageLength )
{
issue . Message = issue . Message [.. _maxIssueMessageLength ];
}
2019-10-10 00:52:42 -04:00
2022-03-14 11:20:11 -04:00
// Tracking the line number (logFileLineNumber) and step number (stepNumber) for each issue that gets created
// Actions UI from the run summary page use both values to easily link to an exact locations in logs where annotations originate from
if ( _record . Order != null )
{
issue . Data [ "stepNumber" ] = _record . Order . ToString ();
}
2019-10-10 00:52:42 -04:00
if ( issue . Type == IssueType . Error )
{
if (! string . IsNullOrEmpty ( logMessage ))
{
long logLineNumber = Write ( WellKnownTags . Error , logMessage );
issue . Data [ "logFileLineNumber" ] = logLineNumber . ToString ();
}
if ( _record . ErrorCount < _maxIssueCount )
{
_record . Issues . Add ( issue );
}
_record . ErrorCount ++;
}
else if ( issue . Type == IssueType . Warning )
{
if (! string . IsNullOrEmpty ( logMessage ))
{
long logLineNumber = Write ( WellKnownTags . Warning , logMessage );
issue . Data [ "logFileLineNumber" ] = logLineNumber . ToString ();
}
if ( _record . WarningCount < _maxIssueCount )
{
_record . Issues . Add ( issue );
}
_record . WarningCount ++;
2021-10-27 09:31:58 -04:00
}
else if ( issue . Type == IssueType . Notice )
2021-07-13 11:38:16 -04:00
{
if (! string . IsNullOrEmpty ( logMessage ))
{
long logLineNumber = Write ( WellKnownTags . Notice , logMessage );
issue . Data [ "logFileLineNumber" ] = logLineNumber . ToString ();
}
if ( _record . NoticeCount < _maxIssueCount )
{
_record . Issues . Add ( issue );
}
_record . NoticeCount ++;
2019-10-10 00:52:42 -04:00
}
_jobServerQueue . QueueTimelineRecordUpdate ( _mainTimelineId , _record );
}
public void UpdateDetailTimelineRecord ( TimelineRecord record )
{
ArgUtil . NotNull ( record , nameof ( record ));
if ( record . RecordType == ExecutionContextType . Job )
{
throw new ArgumentOutOfRangeException ( nameof ( record ));
}
if ( _detailTimelineId == Guid . Empty )
{
// create detail timeline
_detailTimelineId = Guid . NewGuid ();
_record . Details = new Timeline ( _detailTimelineId );
_jobServerQueue . QueueTimelineRecordUpdate ( _mainTimelineId , _record );
}
TimelineRecord existRecord ;
if ( _detailRecords . TryGetValue ( record . Id , out existRecord ))
{
existRecord . Name = record . Name ?? existRecord . Name ;
existRecord . RecordType = record . RecordType ?? existRecord . RecordType ;
existRecord . Order = record . Order ?? existRecord . Order ;
existRecord . ParentId = record . ParentId ?? existRecord . ParentId ;
existRecord . StartTime = record . StartTime ?? existRecord . StartTime ;
existRecord . FinishTime = record . FinishTime ?? existRecord . FinishTime ;
existRecord . PercentComplete = record . PercentComplete ?? existRecord . PercentComplete ;
existRecord . CurrentOperation = record . CurrentOperation ?? existRecord . CurrentOperation ;
existRecord . Result = record . Result ?? existRecord . Result ;
existRecord . ResultCode = record . ResultCode ?? existRecord . ResultCode ;
existRecord . State = record . State ?? existRecord . State ;
_jobServerQueue . QueueTimelineRecordUpdate ( _detailTimelineId , existRecord );
}
else
{
_detailRecords [ record . Id ] = record ;
_jobServerQueue . QueueTimelineRecordUpdate ( _detailTimelineId , record );
}
}
public void UpdateTimelineRecordDisplayName ( string displayName )
{
ArgUtil . NotNull ( displayName , nameof ( displayName ));
_record . Name = displayName ;
_jobServerQueue . QueueTimelineRecordUpdate ( _mainTimelineId , _record );
}
public void InitializeJob ( Pipelines . AgentJobRequestMessage message , CancellationToken token )
{
// Validation
Trace . Entering ();
ArgUtil . NotNull ( message , nameof ( message ));
ArgUtil . NotNull ( message . Resources , nameof ( message . Resources ));
ArgUtil . NotNull ( message . Variables , nameof ( message . Variables ));
ArgUtil . NotNull ( message . Plan , nameof ( message . Plan ));
_cancellationTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( token );
2020-07-19 19:05:47 -04:00
Global = new GlobalContext ();
2020-06-09 08:53:28 -04:00
// Plan
2020-07-19 19:05:47 -04:00
Global . Plan = message . Plan ;
Global . Features = PlanUtil . GetFeatures ( message . Plan );
2019-10-10 00:52:42 -04:00
// Endpoints
2020-07-19 19:05:47 -04:00
Global . Endpoints = message . Resources . Endpoints ;
2019-10-10 00:52:42 -04:00
2022-10-31 19:21:33 +05:30
// Ser debug using vars context if debug variables are not already present.
var variables = message . Variables ;
SetDebugUsingVars ( variables , message . ContextData );
Global . Variables = new Variables ( HostContext , variables );
2019-10-10 00:52:42 -04:00
2022-02-14 15:06:08 +01:00
if ( Global . Variables . GetBoolean ( "DistributedTask.ForceInternalNodeVersionOnRunnerTo12" ) ?? false )
{
2022-02-25 20:59:02 +01:00
Environment . SetEnvironmentVariable ( Constants . Variables . Agent . ForcedInternalNodeVersion , "node12" );
2022-02-14 15:06:08 +01:00
}
2019-10-10 00:52:42 -04:00
// Environment variables shared across all actions
2020-07-19 19:05:47 -04:00
Global . EnvironmentVariables = new Dictionary < string , string >( VarUtil . EnvironmentVariableKeyComparer );
2019-10-10 00:52:42 -04:00
2020-03-17 23:40:37 -04:00
// Job defaults shared across all actions
2020-07-19 19:05:47 -04:00
Global . JobDefaults = new Dictionary < string , IDictionary < string , string >>( StringComparer . OrdinalIgnoreCase );
2020-03-17 23:40:37 -04:00
2022-02-11 16:18:41 -05:00
// Job Telemetry
Global . JobTelemetry = new List < JobTelemetry >();
// ActionsStepTelemetry for entire job
Global . StepsTelemetry = new List < ActionsStepTelemetry >();
2020-03-14 17:54:58 -04:00
// Job Outputs
JobOutputs = new Dictionary < string , VariableValue >( StringComparer . OrdinalIgnoreCase );
2020-10-21 12:14:21 -04:00
// Actions environment
ActionsEnvironment = message . ActionsEnvironment ;
2021-09-20 14:44:50 +02:00
2019-10-10 00:52:42 -04:00
// Service container info
2020-07-19 19:05:47 -04:00
Global . ServiceContainers = new List < ContainerInfo >();
2019-10-10 00:52:42 -04:00
// Steps context (StepsRunner manages adding the scoped steps context)
2020-07-19 19:05:47 -04:00
Global . StepsContext = new StepsContext ();
2019-10-10 00:52:42 -04:00
2020-03-03 22:38:19 -05:00
// File table
2020-07-19 19:05:47 -04:00
Global . FileTable = new List < String >( message . FileTable ?? new string [ 0 ]);
2020-03-03 22:38:19 -05:00
2019-10-10 00:52:42 -04:00
// Expression values
if ( message . ContextData ?. Count > 0 )
{
foreach ( var pair in message . ContextData )
{
ExpressionValues [ pair . Key ] = pair . Value ;
}
}
2020-07-19 19:05:47 -04:00
ExpressionValues [ "secrets" ] = Global . Variables . ToSecretsContext ();
2019-10-10 00:52:42 -04:00
ExpressionValues [ "runner" ] = new RunnerContext ();
ExpressionValues [ "job" ] = new JobContext ();
Trace . Info ( "Initialize GitHub context" );
2020-07-19 19:05:47 -04:00
var githubAccessToken = new StringContextData ( Global . Variables . Get ( "system.github.token" ));
2019-10-10 00:52:42 -04:00
var base64EncodedToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"x-access-token:{githubAccessToken}" ));
HostContext . SecretMasker . AddValue ( base64EncodedToken );
2020-07-19 19:05:47 -04:00
var githubJob = Global . Variables . Get ( "system.github.job" );
2019-10-10 00:52:42 -04:00
var githubContext = new GitHubContext ();
githubContext [ "token" ] = githubAccessToken ;
2020-03-12 20:47:25 -04:00
if (! string . IsNullOrEmpty ( githubJob ))
{
githubContext [ "job" ] = new StringContextData ( githubJob );
}
2019-10-10 00:52:42 -04:00
var githubDictionary = ExpressionValues [ "github" ]. AssertDictionary ( "github" );
foreach ( var pair in githubDictionary )
{
githubContext [ pair . Key ] = pair . Value ;
}
ExpressionValues [ "github" ] = githubContext ;
Trace . Info ( "Initialize Env context" );
#if OS_WINDOWS
ExpressionValues [ "env" ] = new DictionaryContextData ();
#else
ExpressionValues [ "env" ] = new CaseSensitiveDictionaryContextData ();
#endif
// Prepend Path
2020-07-19 19:05:47 -04:00
Global . PrependPath = new List < string >();
2019-10-10 00:52:42 -04:00
// JobSteps for job ExecutionContext
2020-07-29 16:19:04 -04:00
JobSteps = new Queue < IStep >();
2019-10-10 00:52:42 -04:00
// PostJobSteps for job ExecutionContext
PostJobSteps = new Stack < IStep >();
2020-04-13 21:46:30 -04:00
// StepsWithPostRegistered for job ExecutionContext
StepsWithPostRegistered = new HashSet < Guid >();
2021-07-28 15:35:21 -04:00
// EmbeddedStepsWithPostRegistered for job ExecutionContext
2021-10-27 09:31:58 -04:00
EmbeddedStepsWithPostRegistered = new Dictionary < Guid , string >();
2021-07-28 15:35:21 -04:00
// EmbeddedIntraActionState for job ExecutionContext
2021-10-27 09:31:58 -04:00
EmbeddedIntraActionState = new Dictionary < Guid , Dictionary < string , string >>();
2021-07-28 15:35:21 -04:00
2019-10-10 00:52:42 -04:00
// Job timeline record.
InitializeTimelineRecord (
timelineId : message . Timeline . Id ,
timelineRecordId : message . JobId ,
parentTimelineRecordId : null ,
recordType : ExecutionContextType . Job ,
displayName : message . JobDisplayName ,
refName : message . JobName ,
order : null ); // The job timeline record's order is set by server.
// Logger (must be initialized before writing warnings).
_logger = HostContext . CreateService < IPagingLogger >();
_logger . Setup ( _mainTimelineId , _record . Id );
2019-10-25 10:38:56 -04:00
// Initialize 'echo on action command success' property, default to false, unless Step_Debug is set
2020-07-19 19:05:47 -04:00
EchoOnActionCommand = Global . Variables . Step_Debug ?? false ;
2019-10-25 10:38:56 -04:00
2019-10-10 00:52:42 -04:00
// Verbosity (from GitHub.Step_Debug).
2020-07-19 19:05:47 -04:00
Global . WriteDebug = Global . Variables . Step_Debug ?? false ;
2019-10-10 00:52:42 -04:00
// Hook up JobServerQueueThrottling event, we will log warning on server tarpit.
_jobServerQueue . JobServerQueueThrottling += JobServerQueueThrottling_EventReceived ;
}
// Do not add a format string overload. In general, execution context messages are user facing and
// therefore should be localized. Use the Loc methods from the StringUtil class. The exception to
// the rule is command messages - which should be crafted using strongly typed wrapper methods.
public long Write ( string tag , string message )
{
string msg = HostContext . SecretMasker . MaskSecrets ( $"{tag}{message}" );
long totalLines ;
lock ( _loggerLock )
{
totalLines = _logger . TotalLines + 1 ;
_logger . Write ( msg );
}
// write to job level execution context's log file.
if ( _parentExecutionContext != null )
{
lock ( _parentExecutionContext . _loggerLock )
{
_parentExecutionContext . _logger . Write ( msg );
}
}
2020-08-25 12:02:29 -04:00
_jobServerQueue . QueueWebConsoleLine ( _record . Id , msg , totalLines );
2019-10-10 00:52:42 -04:00
return totalLines ;
}
public void QueueAttachFile ( string type , string name , string filePath )
{
ArgUtil . NotNullOrEmpty ( type , nameof ( type ));
ArgUtil . NotNullOrEmpty ( name , nameof ( name ));
ArgUtil . NotNullOrEmpty ( filePath , nameof ( filePath ));
if (! File . Exists ( filePath ))
{
throw new FileNotFoundException ( $"Can't attach (type:{type} name:{name}) file: {filePath}. File does not exist." );
}
_jobServerQueue . QueueFileUpload ( _mainTimelineId , _record . Id , type , name , filePath , deleteSource : false );
}
2022-12-28 11:56:53 -05:00
public void QueueSummaryFile ( string name , string filePath , Guid stepRecordId )
2022-12-14 00:28:33 -08:00
{
ArgUtil . NotNullOrEmpty ( name , nameof ( name ));
ArgUtil . NotNullOrEmpty ( filePath , nameof ( filePath ));
if (! File . Exists ( filePath ))
{
throw new FileNotFoundException ( $"Can't upload (name:{name}) file: {filePath}. File does not exist." );
}
2022-12-28 11:56:53 -05:00
_jobServerQueue . QueueSummaryUpload ( stepRecordId , name , filePath , deleteSource : false );
2022-12-14 00:28:33 -08:00
}
2019-10-10 00:52:42 -04:00
// Add OnMatcherChanged
public void Add ( OnMatcherChanged handler )
{
Root . _onMatcherChanged += handler ;
}
// Remove OnMatcherChanged
public void Remove ( OnMatcherChanged handler )
{
Root . _onMatcherChanged -= handler ;
}
// Add Issue matchers
public void AddMatchers ( IssueMatchersConfig config )
{
var root = Root ;
// Lock
lock ( root . _matchersLock )
{
var newMatchers = new List < IssueMatcherConfig >();
// Prepend
var newOwners = new HashSet < string >( StringComparer . OrdinalIgnoreCase );
foreach ( var matcher in config . Matchers )
{
newOwners . Add ( matcher . Owner );
newMatchers . Add ( matcher );
}
// Add existing non-matching
var existingMatchers = root . _matchers ?? Array . Empty < IssueMatcherConfig >();
newMatchers . AddRange ( existingMatchers . Where ( x => ! newOwners . Contains ( x . Owner )));
// Store
root . _matchers = newMatchers . ToArray ();
// Fire events
foreach ( var matcher in config . Matchers )
{
root . _onMatcherChanged ( null , new MatcherChangedEventArgs ( matcher ));
}
// Output
var owners = config . Matchers . Select ( x => $"'{x.Owner}'" );
var joinedOwners = string . Join ( ", " , owners );
// todo: loc
2020-03-12 02:52:46 +01:00
this . Debug ( $"Added matchers: {joinedOwners}. Problem matchers scan action output for known warning or error strings and report these inline." );
2019-10-10 00:52:42 -04:00
}
}
// Remove issue matcher
public void RemoveMatchers ( IEnumerable < string > owners )
{
var root = Root ;
var distinctOwners = new HashSet < string >( owners , StringComparer . OrdinalIgnoreCase );
var removedMatchers = new List < IssueMatcherConfig >();
var newMatchers = new List < IssueMatcherConfig >();
// Lock
lock ( root . _matchersLock )
{
// Remove
var existingMatchers = root . _matchers ?? Array . Empty < IssueMatcherConfig >();
foreach ( var matcher in existingMatchers )
{
if ( distinctOwners . Contains ( matcher . Owner ))
{
removedMatchers . Add ( matcher );
}
else
{
newMatchers . Add ( matcher );
}
}
// Store
root . _matchers = newMatchers . ToArray ();
// Fire events
foreach ( var removedMatcher in removedMatchers )
{
root . _onMatcherChanged ( null , new MatcherChangedEventArgs ( new IssueMatcherConfig { Owner = removedMatcher . Owner }));
}
// Output
owners = removedMatchers . Select ( x => $"'{x.Owner}'" );
var joinedOwners = string . Join ( ", " , owners );
// todo: loc
2020-03-12 02:52:46 +01:00
this . Debug ( $"Removed matchers: {joinedOwners}" );
2019-10-10 00:52:42 -04:00
}
}
// Get issue matchers
public IEnumerable < IssueMatcherConfig > GetMatchers ()
{
// Lock not required since the list is immutable
return Root . _matchers ?? Array . Empty < IssueMatcherConfig >();
}
2022-02-11 16:18:41 -05:00
public void PublishStepTelemetry ()
{
if (! _stepTelemetryPublished )
{
// Add to the global steps telemetry only if we have something to log.
if (! string . IsNullOrEmpty ( StepTelemetry ?. Type ))
{
2022-02-16 12:18:21 -05:00
if (! IsEmbedded )
{
StepTelemetry . Result = _record . Result ;
}
if (! IsEmbedded &&
_record . FinishTime != null &&
_record . StartTime != null )
{
StepTelemetry . ExecutionTimeInSeconds = ( int ) Math . Ceiling (( _record . FinishTime - _record . StartTime )?. TotalSeconds ?? 0 );
2022-08-22 18:26:52 -07:00
StepTelemetry . StartTime = _record . StartTime ;
StepTelemetry . FinishTime = _record . FinishTime ;
2022-02-16 12:18:21 -05:00
}
if (! IsEmbedded &&
_record . Issues . Count > 0 )
{
foreach ( var issue in _record . Issues )
{
if (( issue . Type == IssueType . Error || issue . Type == IssueType . Warning ) &&
! string . IsNullOrEmpty ( issue . Message ))
{
string issueTelemetry ;
if ( issue . Message . Length > _maxIssueMessageLengthInTelemetry )
{
issueTelemetry = $"{issue.Message[.._maxIssueMessageLengthInTelemetry]}" ;
}
else
{
issueTelemetry = issue . Message ;
}
StepTelemetry . ErrorMessages . Add ( issueTelemetry );
// Only send over the first 3 issues to avoid sending too much data.
if ( StepTelemetry . ErrorMessages . Count >= _maxIssueCountInTelemetry )
{
break ;
}
}
}
}
2022-02-11 16:18:41 -05:00
Trace . Info ( $"Publish step telemetry for current step {StringUtil.ConvertToJson(StepTelemetry)}." );
Global . StepsTelemetry . Add ( StepTelemetry );
_stepTelemetryPublished = true ;
}
}
else
{
Trace . Info ( $"Step telemetry has already been published." );
}
}
2022-03-18 02:35:04 +01:00
public void WriteWebhookPayload ()
{
// Makes directory for event_path data
var tempDirectory = HostContext . GetDirectory ( WellKnownDirectory . Temp );
var workflowDirectory = Path . Combine ( tempDirectory , "_github_workflow" );
Directory . CreateDirectory ( workflowDirectory );
var gitHubEvent = GetGitHubContext ( "event" );
// adds the GitHub event path/file if the event exists
if ( gitHubEvent != null )
{
var workflowFile = Path . Combine ( workflowDirectory , "event.json" );
Trace . Info ( $"Write event payload to {workflowFile}" );
File . WriteAllText ( workflowFile , gitHubEvent , new UTF8Encoding ( false ));
SetGitHubContext ( "event_path" , workflowFile );
}
}
2019-10-10 00:52:42 -04:00
private void InitializeTimelineRecord ( Guid timelineId , Guid timelineRecordId , Guid ? parentTimelineRecordId , string recordType , string displayName , string refName , int? order )
{
_mainTimelineId = timelineId ;
_record . Id = timelineRecordId ;
_record . RecordType = recordType ;
_record . Name = displayName ;
_record . RefName = refName ;
_record . Order = order ;
_record . PercentComplete = 0 ;
_record . State = TimelineRecordState . Pending ;
_record . ErrorCount = 0 ;
_record . WarningCount = 0 ;
2021-07-13 11:38:16 -04:00
_record . NoticeCount = 0 ;
2019-10-10 00:52:42 -04:00
if ( parentTimelineRecordId != null && parentTimelineRecordId . Value != Guid . Empty )
{
_record . ParentId = parentTimelineRecordId ;
}
2021-01-25 11:14:28 -05:00
else if ( parentTimelineRecordId == null )
{
_record . AgentPlatform = VarUtil . OS ;
}
2019-10-10 00:52:42 -04:00
var configuration = HostContext . GetService < IConfigurationStore >();
_record . WorkerName = configuration . GetSettings (). AgentName ;
_jobServerQueue . QueueTimelineRecordUpdate ( _mainTimelineId , _record );
}
private void JobServerQueueThrottling_EventReceived ( object sender , ThrottlingEventArgs data )
{
Interlocked . Add ( ref _totalThrottlingDelayInMilliseconds , Convert . ToInt64 ( data . Delay . TotalMilliseconds ));
2020-05-21 11:09:50 -04:00
if (! _throttlingReported &&
2020-05-12 16:09:13 -04:00
_totalThrottlingDelayInMilliseconds > _throttlingDelayReportThreshold )
2019-10-10 00:52:42 -04:00
{
this . Warning ( string . Format ( "The job is currently being throttled by the server. You may experience delays in console line output, job status reporting, and action log uploads." ));
_throttlingReported = true ;
}
}
2021-08-04 11:39:22 -04:00
private IExecutionContext CreatePostChild ( string displayName , Dictionary < string , string > intraActionState , string siblingScopeName = null )
2019-10-10 00:52:42 -04:00
{
if (! _expandedForPostJob )
{
Trace . Info ( $"Reserve record order {_childTimelineRecordOrder + 1} to {_childTimelineRecordOrder * 2} for post job actions." );
_expandedForPostJob = true ;
_childTimelineRecordOrder = _childTimelineRecordOrder * 2 ;
}
2020-04-13 21:46:30 -04:00
var newGuid = Guid . NewGuid ();
2021-10-29 15:45:42 +02:00
return CreateChild ( newGuid , displayName , newGuid . ToString ( "N" ), null , null , ActionRunStage . Post , intraActionState , _childTimelineRecordOrder - Root . PostJobSteps . Count , siblingScopeName : siblingScopeName );
2019-10-10 00:52:42 -04:00
}
2022-04-01 15:18:53 +02:00
2022-10-31 19:21:33 +05:30
// Sets debug using vars context in case debug variables are not present.
private static void SetDebugUsingVars ( IDictionary < string , VariableValue > variables , IDictionary < string , PipelineContextData > contextData )
{
if ( contextData != null &&
contextData . TryGetValue ( PipelineTemplateConstants . Vars , out var varsPipelineContextData ) &&
2022-12-14 00:28:33 -08:00
varsPipelineContextData != null &&
2022-10-31 19:21:33 +05:30
varsPipelineContextData is DictionaryContextData varsContextData )
{
// Set debug variables only when StepDebug/RunnerDebug variables are not present.
if (! variables . ContainsKey ( Constants . Variables . Actions . StepDebug ) &&
varsContextData . TryGetValue ( Constants . Variables . Actions . StepDebug , out var stepDebugValue ) &&
stepDebugValue is StringContextData )
{
variables [ Constants . Variables . Actions . StepDebug ] = stepDebugValue . ToString ();
}
if (! variables . ContainsKey ( Constants . Variables . Actions . RunnerDebug ) &&
varsContextData . TryGetValue ( Constants . Variables . Actions . RunnerDebug , out var runDebugValue ) &&
runDebugValue is StringContextData )
{
variables [ Constants . Variables . Actions . RunnerDebug ] = runDebugValue . ToString ();
}
}
}
2022-04-01 15:18:53 +02:00
public void ApplyContinueOnError ( TemplateToken continueOnErrorToken )
{
if ( Result != TaskResult . Failed )
{
return ;
}
var continueOnError = false ;
try
{
var templateEvaluator = this . ToPipelineTemplateEvaluator ();
continueOnError = templateEvaluator . EvaluateStepContinueOnError ( continueOnErrorToken , ExpressionValues , ExpressionFunctions );
}
catch ( Exception ex )
{
Trace . Info ( "The step failed and an error occurred when attempting to determine whether to continue on error." );
Trace . Error ( ex );
this . Error ( "The step failed and an error occurred when attempting to determine whether to continue on error." );
this . Error ( ex );
}
if ( continueOnError )
{
Outcome = Result ;
Result = TaskResult . Succeeded ;
Trace . Info ( $"Updated step result (continue on error)" );
}
UpdateGlobalStepsContext ();
}
2019-10-10 00:52:42 -04:00
}
// The Error/Warning/etc methods are created as extension methods to simplify unit testing.
// Otherwise individual overloads would need to be implemented (depending on the unit test).
public static class ExecutionContextExtension
{
2020-07-19 17:19:13 -04:00
public static string GetFullyQualifiedContextName ( this IExecutionContext context )
{
if (! string . IsNullOrEmpty ( context . ScopeName ))
{
return $"{context.ScopeName}.{context.ContextName}" ;
}
return context . ContextName ;
}
2019-10-10 00:52:42 -04:00
public static void Error ( this IExecutionContext context , Exception ex )
{
context . Error ( ex . Message );
context . Debug ( ex . ToString ());
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Error ( this IExecutionContext context , string message )
{
context . AddIssue ( new Issue () { Type = IssueType . Error , Message = message });
}
2020-12-11 11:07:43 -05:00
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void InfrastructureError ( this IExecutionContext context , string message )
{
2021-10-27 09:31:58 -04:00
context . AddIssue ( new Issue () { Type = IssueType . Error , Message = message , IsInfrastructureIssue = true });
2020-12-11 11:07:43 -05:00
}
2019-10-10 00:52:42 -04:00
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Warning ( this IExecutionContext context , string message )
{
context . AddIssue ( new Issue () { Type = IssueType . Warning , Message = message });
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Output ( this IExecutionContext context , string message )
{
context . Write ( null , message );
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Command ( this IExecutionContext context , string message )
{
context . Write ( WellKnownTags . Command , message );
}
//
// Verbose output is enabled by setting ACTIONS_STEP_DEBUG
// It's meant to help the end user debug their definitions.
// Why are my inputs not working? It's not meant for dev debugging which is diag
//
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Debug ( this IExecutionContext context , string message )
{
2020-07-19 19:05:47 -04:00
if ( context . Global . WriteDebug )
2019-10-10 00:52:42 -04:00
{
var multilines = message ?. Replace ( "\r\n" , "\n" )?. Split ( "\n" );
if ( multilines != null )
{
foreach ( var line in multilines )
{
context . Write ( WellKnownTags . Debug , line );
}
}
}
}
2020-03-18 12:08:51 -04:00
public static IEnumerable < KeyValuePair < string , object >> ToExpressionState ( this IExecutionContext context )
2020-03-03 22:38:19 -05:00
{
2020-03-18 12:08:51 -04:00
return new [] { new KeyValuePair < string , object >( nameof ( IExecutionContext ), context ) };
}
public static PipelineTemplateEvaluator ToPipelineTemplateEvaluator ( this IExecutionContext context , ObjectTemplating . ITraceWriter traceWriter = null )
{
if ( traceWriter == null )
{
traceWriter = context . ToTemplateTraceWriter ();
}
var schema = PipelineTemplateSchemaFactory . GetSchema ();
2021-04-06 15:45:40 -05:00
return new PipelineTemplateEvaluator ( traceWriter , schema , context . Global . FileTable )
{
MaxErrorMessageLength = int . MaxValue , // Don't truncate error messages otherwise we might not scrub secrets correctly
};
2020-03-03 22:38:19 -05:00
}
2019-10-10 00:52:42 -04:00
public static ObjectTemplating . ITraceWriter ToTemplateTraceWriter ( this IExecutionContext context )
{
return new TemplateTraceWriter ( context );
}
2022-04-11 14:43:24 +02:00
public static DictionaryContextData GetExpressionValues ( this IExecutionContext context , IStepHost stepHost )
{
if ( stepHost is ContainerStepHost )
{
var expressionValues = context . ExpressionValues . Clone () as DictionaryContextData ;
context . UpdatePathsInExpressionValues ( "github" , expressionValues , stepHost );
context . UpdatePathsInExpressionValues ( "runner" , expressionValues , stepHost );
return expressionValues ;
}
else
{
return context . ExpressionValues . Clone () as DictionaryContextData ;
}
}
private static void UpdatePathsInExpressionValues ( this IExecutionContext context , string contextName , DictionaryContextData expressionValues , IStepHost stepHost )
{
var dict = expressionValues [ contextName ]. AssertDictionary ( $"expected context {contextName} to be a dictionary" );
context . ResolvePathsInExpressionValuesDictionary ( dict , stepHost );
expressionValues [ contextName ] = dict ;
}
private static void ResolvePathsInExpressionValuesDictionary ( this IExecutionContext context , DictionaryContextData dict , IStepHost stepHost )
{
foreach ( var key in dict . Keys . ToList ())
{
if ( dict [ key ] is StringContextData )
{
var value = dict [ key ]. ToString ();
if (! string . IsNullOrEmpty ( value ))
{
2022-06-10 15:51:20 +02:00
dict [ key ] = new StringContextData ( stepHost . ResolvePathForStepHost ( context , value ));
2022-04-11 14:43:24 +02:00
}
}
else if ( dict [ key ] is DictionaryContextData )
{
var innerDict = dict [ key ]. AssertDictionary ( "expected dictionary" );
context . ResolvePathsInExpressionValuesDictionary ( innerDict , stepHost );
var updatedDict = new DictionaryContextData ();
foreach ( var k in innerDict . Keys . ToList ())
{
updatedDict [ k ] = innerDict [ k ];
}
dict [ key ] = updatedDict ;
}
else if ( dict [ key ] is CaseSensitiveDictionaryContextData )
{
var innerDict = dict [ key ]. AssertDictionary ( "expected dictionary" );
context . ResolvePathsInExpressionValuesDictionary ( innerDict , stepHost );
var updatedDict = new CaseSensitiveDictionaryContextData ();
foreach ( var k in innerDict . Keys . ToList ())
{
updatedDict [ k ] = innerDict [ k ];
}
dict [ key ] = updatedDict ;
}
}
}
2019-10-10 00:52:42 -04:00
}
internal sealed class TemplateTraceWriter : ObjectTemplating . ITraceWriter
{
private readonly IExecutionContext _executionContext ;
internal TemplateTraceWriter ( IExecutionContext executionContext )
{
2020-03-18 12:08:51 -04:00
ArgUtil . NotNull ( executionContext , nameof ( executionContext ));
2019-10-10 00:52:42 -04:00
_executionContext = executionContext ;
}
public void Error ( string format , params Object [] args )
{
_executionContext . Error ( string . Format ( CultureInfo . CurrentCulture , format , args ));
}
public void Info ( string format , params Object [] args )
{
_executionContext . Debug ( string . Format ( CultureInfo . CurrentCulture , $"{format}" , args ));
}
public void Verbose ( string format , params Object [] args )
{
// todo: switch to verbose?
_executionContext . Debug ( string . Format ( CultureInfo . CurrentCulture , $"{format}" , args ));
}
}
public static class WellKnownTags
{
public static readonly string Section = "##[section]" ;
public static readonly string Command = "##[command]" ;
public static readonly string Error = "##[error]" ;
public static readonly string Warning = "##[warning]" ;
2021-07-13 11:38:16 -04:00
public static readonly string Notice = "##[notice]" ;
2019-10-10 00:52:42 -04:00
public static readonly string Debug = "##[debug]" ;
}
}