diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index dea9e7dc0..49f9d3e73 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -228,7 +228,30 @@ namespace GitHub.Runner.Worker { throw new Exception($"Missing download info for {lookupKey}"); } - await DownloadRepositoryActionAsync(executionContext, downloadInfo); + + Exception downloadFailure = null; + try + { + await DownloadRepositoryActionAsync(executionContext, downloadInfo); + } + catch (Exception ex) + { + // record the exception for telemetry, and rethrow the original exception to fail the step. + downloadFailure = ex; + throw; + } + finally + { + executionContext.Global.JobTelemetry.Add(new JobTelemetry() + { + Type = JobTelemetryType.General, + Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload + { + Operation = "download_action", + Result = downloadFailure == null ? "succeeded" : downloadFailure.GetType().Name + }, Newtonsoft.Json.Formatting.None)}" + }); + } } // Parse action.yml and collect composite sub-actions for batched @@ -398,7 +421,30 @@ namespace GitHub.Runner.Worker if (repositoryActions.Count > 0) { // Get the download info - var downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions); + IDictionary downloadInfos = null; + Exception resolveFailure = null; + try + { + downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions); + } + catch (Exception ex) + { + // record the exception for telemetry, and rethrow the original exception to fail the step. + resolveFailure = ex; + throw; + } + finally + { + executionContext.Global.JobTelemetry.Add(new JobTelemetry() + { + Type = JobTelemetryType.General, + Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload + { + Operation = "resolve_actions", + Result = resolveFailure == null ? "succeeded" : resolveFailure.GetType().Name + }, Newtonsoft.Json.Formatting.None)}" + }); + } // Download each action foreach (var action in repositoryActions) @@ -414,7 +460,29 @@ namespace GitHub.Runner.Worker throw new Exception($"Missing download info for {lookupKey}"); } - await DownloadRepositoryActionAsync(executionContext, downloadInfo); + Exception downloadFailure = null; + try + { + await DownloadRepositoryActionAsync(executionContext, downloadInfo); + } + catch (Exception ex) + { + // record the exception for telemetry, and rethrow the original exception to fail the step. + downloadFailure = ex; + throw; + } + finally + { + executionContext.Global.JobTelemetry.Add(new JobTelemetry() + { + Type = JobTelemetryType.General, + Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload + { + Operation = "download_action", + Result = downloadFailure == null ? "succeeded" : downloadFailure.GetType().Name + }, Newtonsoft.Json.Formatting.None)}" + }); + } } // More preparation based on content in the repository (action.yml) @@ -980,10 +1048,33 @@ namespace GitHub.Runner.Worker if (actionsToResolve.Count > 0) { - var downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve); - foreach (var kvp in downloadInfos) + IDictionary downloadInfos = null; + Exception resolveFailure = null; + try { - resolvedDownloadInfos[kvp.Key] = kvp.Value; + downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve); + foreach (var kvp in downloadInfos) + { + resolvedDownloadInfos[kvp.Key] = kvp.Value; + } + } + catch (Exception ex) + { + // record the exception for telemetry, and rethrow the original exception to fail the step. + resolveFailure = ex; + throw; + } + finally + { + executionContext.Global.JobTelemetry.Add(new JobTelemetry() + { + Type = JobTelemetryType.General, + Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload + { + Operation = "resolve_actions", + Result = resolveFailure == null ? "succeeded" : resolveFailure.GetType().Name + }, Newtonsoft.Json.Formatting.None)}" + }); } } } @@ -1209,6 +1300,12 @@ namespace GitHub.Runner.Worker private string GetWatermarkFilePath(string directory) => directory + ".completed"; + private sealed class ActionTelemetryPayload + { + public string Operation { get; set; } + public string Result { get; set; } + } + private ActionSetupInfo PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction) { var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference; diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index c612ac9d0..05c69845a 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -90,6 +90,11 @@ namespace GitHub.Runner.Common.Tests.Worker var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main", "action.yml"); Assert.True(File.Exists(actionYamlFile)); + + var telemetryMessages = GetTelemetryMessages(); + Assert.True(ContainsTelemetry(telemetryMessages, "resolve_actions")); + Assert.True(ContainsTelemetry(telemetryMessages, "succeeded")); + Assert.True(ContainsTelemetry(telemetryMessages, "download_action")); _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); } finally @@ -148,6 +153,11 @@ namespace GitHub.Runner.Common.Tests.Worker // Act + Assert await Assert.ThrowsAsync(async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions)); + + var telemetryMessages = GetTelemetryMessages(); + Assert.True(ContainsTelemetry(telemetryMessages, "resolve_actions")); + Assert.True(ContainsTelemetry(telemetryMessages, "download_action")); + Assert.True(ContainsTelemetry(telemetryMessages, "InvalidActionArchiveException")); } finally { @@ -215,6 +225,51 @@ namespace GitHub.Runner.Common.Tests.Worker } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task PrepareActions_ResolveActionDownloadInfo_RecordsTelemetry_OnFailure() + { + try + { + // Arrange + Setup(); + _ec.Object.Global.Variables.Set(Constants.Variables.System.JobRequestType, "RunnerJobRequest"); + + _launchServer + .Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("resolve failed")); + + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "actions/checkout", + Ref = "v4", + RepositoryType = "GitHub" + } + } + }; + + // Act + Assert + await Assert.ThrowsAsync(async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions)); + + var telemetryMessages = GetTelemetryMessages(); + Assert.Equal(1, telemetryMessages.Count(message => + message.Contains("resolve_actions", StringComparison.OrdinalIgnoreCase) + && !message.Contains("\"result\":\"succeeded\"", StringComparison.OrdinalIgnoreCase))); + Assert.False(ContainsTelemetry(telemetryMessages, "resolve_actions\",\"result\":\"succeeded")); + } + finally + { + Teardown(); + } + } + #if OS_LINUX [Fact] [Trait("Level", "L0")] @@ -3333,6 +3388,16 @@ runs: } } + private IList GetTelemetryMessages() + { + return _ec.Object.Global.JobTelemetry.Select(x => x.Message).ToList(); + } + + private static bool ContainsTelemetry(IList telemetryMessages, string expectedFragment) + { + return telemetryMessages.Any(message => message.Contains(expectedFragment, StringComparison.OrdinalIgnoreCase)); + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")]