using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using GitHub.DistributedTask.Expressions2; using GitHub.DistributedTask.Pipelines; using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.DistributedTask.Pipelines.ObjectTemplating; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common.Util; using GitHub.Runner.Common; using GitHub.Runner.Sdk; using GitHub.Runner.Worker.Container; using GitHub.Services.WebApi; using Newtonsoft.Json; using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating; using Pipelines = GitHub.DistributedTask.Pipelines; 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; } string ScopeName { get; } string ContextName { get; } Task ForceCompleted { get; } TaskResult? Result { get; set; } TaskResult? Outcome { get; set; } string ResultCode { get; set; } TaskResult? CommandResult { get; set; } CancellationToken CancellationToken { get; } List Endpoints { get; } PlanFeatures Features { get; } Variables Variables { get; } Dictionary IntraActionState { get; } IDictionary> JobDefaults { get; } Dictionary JobOutputs { get; } IDictionary EnvironmentVariables { get; } IDictionary Scopes { get; } IList FileTable { get; } StepsContext StepsContext { get; } DictionaryContextData ExpressionValues { get; } IList ExpressionFunctions { get; } List PrependPath { get; } ContainerInfo Container { get; set; } List ServiceContainers { get; } JobContext JobContext { get; } // Only job level ExecutionContext has JobSteps Queue JobSteps { get; } // Only job level ExecutionContext has PostJobSteps Stack PostJobSteps { get; } bool EchoOnActionCommand { get; set; } // Initialize void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token); void CancelToken(); IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, Dictionary intraActionState = null, int? recordOrder = null); // logging bool WriteDebug { get; } long Write(string tag, string message); void QueueAttachFile(string type, string name, string filePath); // 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 owners); IEnumerable GetMatchers(); // others void ForceTaskComplete(); void RegisterPostJobStep(IStep step); } public sealed class ExecutionContext : RunnerService, IExecutionContext { private const int _maxIssueCount = 10; private readonly TimelineRecord _record = new TimelineRecord(); private readonly Dictionary _detailRecords = new Dictionary(); private readonly object _loggerLock = new object(); private readonly object _matchersLock = new object(); 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; private TaskCompletionSource _forceCompleted = new TaskCompletionSource(); private bool _throttlingReported = false; // only job level ExecutionContext will track throttling delay. private long _totalThrottlingDelayInMilliseconds = 0; public Guid Id => _record.Id; public string ScopeName { get; private set; } public string ContextName { get; private set; } public Task ForceCompleted => _forceCompleted.Task; public CancellationToken CancellationToken => _cancellationTokenSource.Token; public List Endpoints { get; private set; } public Variables Variables { get; private set; } public Dictionary IntraActionState { get; private set; } public IDictionary> JobDefaults { get; private set; } public Dictionary JobOutputs { get; private set; } public IDictionary EnvironmentVariables { get; private set; } public IDictionary Scopes { get; private set; } public IList FileTable { get; private set; } public StepsContext StepsContext { get; private set; } public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData(); public IList ExpressionFunctions { get; } = new List(); public bool WriteDebug { get; private set; } public List PrependPath { get; private set; } public ContainerInfo Container { get; set; } public List ServiceContainers { get; private set; } // Only job level ExecutionContext has JobSteps public Queue JobSteps { get; private set; } // Only job level ExecutionContext has PostJobSteps public Stack PostJobSteps { get; private set; } // Only job level ExecutionContext has StepsWithPostRegistered public HashSet StepsWithPostRegistered { get; private set; } public bool EchoOnActionCommand { get; set; } public TaskResult? Result { get { return _record.Result; } set { _record.Result = value; } } public TaskResult? Outcome { get; set; } public TaskResult? CommandResult { get; set; } private string ContextType => _record.RecordType; public string ResultCode { get { return _record.ResultCode; } set { _record.ResultCode = value; } } public PlanFeatures Features { get; private set; } private ExecutionContext Root { get { var result = this; while (result._parentExecutionContext != null) { result = result._parentExecutionContext; } return result; } } public JobContext JobContext { get { return ExpressionValues["job"] as JobContext; } } public override void Initialize(IHostContext hostContext) { base.Initialize(hostContext); _jobServerQueue = HostContext.GetService(); } 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); }); } public void RegisterPostJobStep(IStep step) { if (step is IActionRunner actionRunner && !Root.StepsWithPostRegistered.Add(actionRunner.Action.Id)) { Trace.Info($"'post' of '{actionRunner.DisplayName}' already push to post step stack."); return; } step.ExecutionContext = Root.CreatePostChild(step.DisplayName, IntraActionState); Root.PostJobSteps.Push(step); } public IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, Dictionary intraActionState = null, int? recordOrder = null) { Trace.Entering(); var child = new ExecutionContext(); child.Initialize(HostContext); child.ScopeName = scopeName; child.ContextName = contextName; child.Features = Features; child.Variables = Variables; child.Endpoints = Endpoints; if (intraActionState == null) { child.IntraActionState = new Dictionary(StringComparer.OrdinalIgnoreCase); } else { child.IntraActionState = intraActionState; } child.EnvironmentVariables = EnvironmentVariables; child.JobDefaults = JobDefaults; child.Scopes = Scopes; child.FileTable = FileTable; child.StepsContext = StepsContext; foreach (var pair in ExpressionValues) { child.ExpressionValues[pair.Key] = pair.Value; } foreach (var item in ExpressionFunctions) { child.ExpressionFunctions.Add(item); } child._cancellationTokenSource = new CancellationTokenSource(); child.WriteDebug = WriteDebug; child._parentExecutionContext = this; child.PrependPath = PrependPath; child.Container = Container; child.ServiceContainers = ServiceContainers; child.EchoOnActionCommand = EchoOnActionCommand; 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); } child._logger = HostContext.CreateService(); child._logger.Setup(_mainTimelineId, recordId); return child; } 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. if (_totalThrottlingDelayInMilliseconds > 0) { 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); } } _cancellationTokenSource?.Dispose(); _logger.End(); if (!string.IsNullOrEmpty(ContextName)) { StepsContext.SetOutcome(ScopeName, ContextName, (Outcome ?? Result ?? TaskResult.Succeeded).ToActionResult().ToString()); StepsContext.SetConclusion(ScopeName, ContextName, (Result ?? TaskResult.Succeeded).ToActionResult().ToString()); } return Result.Value; } 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)); if (String.IsNullOrEmpty(ContextName)) { reference = null; return; } // todo: restrict multiline? StepsContext.SetOutput(ScopeName, ContextName, name, value, out reference); } 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); if (issue.Type == IssueType.Error) { // tracking line number for each issue in log file // log UI use this to navigate from issue to log 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) { // tracking line number for each issue in log file // log UI use this to navigate from issue to log 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++; } _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); // Features Features = PlanUtil.GetFeatures(message.Plan); // Endpoints Endpoints = message.Resources.Endpoints; // Variables Variables = new Variables(HostContext, message.Variables); // Environment variables shared across all actions EnvironmentVariables = new Dictionary(VarUtil.EnvironmentVariableKeyComparer); // Job defaults shared across all actions JobDefaults = new Dictionary>(StringComparer.OrdinalIgnoreCase); // Job Outputs JobOutputs = new Dictionary(StringComparer.OrdinalIgnoreCase); // Service container info ServiceContainers = new List(); // Steps context (StepsRunner manages adding the scoped steps context) StepsContext = new StepsContext(); // Scopes Scopes = new Dictionary(StringComparer.OrdinalIgnoreCase); if (message.Scopes?.Count > 0) { foreach (var scope in message.Scopes) { Scopes[scope.Name] = scope; } } // File table FileTable = new List(message.FileTable ?? new string[0]); // Expression values if (message.ContextData?.Count > 0) { foreach (var pair in message.ContextData) { ExpressionValues[pair.Key] = pair.Value; } } ExpressionValues["secrets"] = Variables.ToSecretsContext(); ExpressionValues["runner"] = new RunnerContext(); ExpressionValues["job"] = new JobContext(); Trace.Info("Initialize GitHub context"); var githubAccessToken = new StringContextData(Variables.Get("system.github.token")); var base64EncodedToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{githubAccessToken}")); HostContext.SecretMasker.AddValue(base64EncodedToken); var githubJob = Variables.Get("system.github.job"); var githubContext = new GitHubContext(); githubContext["token"] = githubAccessToken; if (!string.IsNullOrEmpty(githubJob)) { githubContext["job"] = new StringContextData(githubJob); } 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 PrependPath = new List(); // JobSteps for job ExecutionContext JobSteps = new Queue(); // PostJobSteps for job ExecutionContext PostJobSteps = new Stack(); // StepsWithPostRegistered for job ExecutionContext StepsWithPostRegistered = new HashSet(); // 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(); _logger.Setup(_mainTimelineId, _record.Id); // Initialize 'echo on action command success' property, default to false, unless Step_Debug is set EchoOnActionCommand = Variables.Step_Debug ?? false; // Verbosity (from GitHub.Step_Debug). WriteDebug = Variables.Step_Debug ?? false; // 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); } } _jobServerQueue.QueueWebConsoleLine(_record.Id, msg); 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); } // 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(); // Prepend var newOwners = new HashSet(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(); 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 this.Debug($"Added matchers: {joinedOwners}. Problem matchers scan action output for known warning or error strings and report these inline."); } } // Remove issue matcher public void RemoveMatchers(IEnumerable owners) { var root = Root; var distinctOwners = new HashSet(owners, StringComparer.OrdinalIgnoreCase); var removedMatchers = new List(); var newMatchers = new List(); // Lock lock (root._matchersLock) { // Remove var existingMatchers = root._matchers ?? Array.Empty(); 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 this.Debug($"Removed matchers: {joinedOwners}"); } } // Get issue matchers public IEnumerable GetMatchers() { // Lock not required since the list is immutable return Root._matchers ?? Array.Empty(); } 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; if (parentTimelineRecordId != null && parentTimelineRecordId.Value != Guid.Empty) { _record.ParentId = parentTimelineRecordId; } var configuration = HostContext.GetService(); _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)); if (!_throttlingReported) { 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; } } private IExecutionContext CreatePostChild(string displayName, Dictionary intraActionState) { if (!_expandedForPostJob) { Trace.Info($"Reserve record order {_childTimelineRecordOrder + 1} to {_childTimelineRecordOrder * 2} for post job actions."); _expandedForPostJob = true; _childTimelineRecordOrder = _childTimelineRecordOrder * 2; } var newGuid = Guid.NewGuid(); return CreateChild(newGuid, displayName, newGuid.ToString("N"), null, null, intraActionState, _childTimelineRecordOrder - Root.PostJobSteps.Count); } } // 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 { 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 }); } // 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) { if (context.WriteDebug) { var multilines = message?.Replace("\r\n", "\n")?.Split("\n"); if (multilines != null) { foreach (var line in multilines) { context.Write(WellKnownTags.Debug, line); } } } } public static IEnumerable> ToExpressionState(this IExecutionContext context) { return new[] { new KeyValuePair(nameof(IExecutionContext), context) }; } public static PipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null) { if (traceWriter == null) { traceWriter = context.ToTemplateTraceWriter(); } var schema = PipelineTemplateSchemaFactory.GetSchema(); return new PipelineTemplateEvaluator(traceWriter, schema, context.FileTable); } public static ObjectTemplating.ITraceWriter ToTemplateTraceWriter(this IExecutionContext context) { return new TemplateTraceWriter(context); } } internal sealed class TemplateTraceWriter : ObjectTemplating.ITraceWriter { private readonly IExecutionContext _executionContext; internal TemplateTraceWriter(IExecutionContext executionContext) { ArgUtil.NotNull(executionContext, nameof(executionContext)); _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]"; public static readonly string Debug = "##[debug]"; } }