mirror of
https://github.com/actions/runner.git
synced 2025-12-11 12:57:05 +00:00
send annotations to run-service (#2574)
* send annotations to run-service * skip message deletion * actually don't skip deletion * enum as numbers * fix enum * linting * remove unncessary file * feedback
This commit is contained in:
committed by
GitHub
parent
8d74a9ead6
commit
896152d78e
@@ -19,7 +19,14 @@ namespace GitHub.Runner.Common
|
||||
|
||||
Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken token);
|
||||
|
||||
Task CompleteJobAsync(Guid planId, Guid jobId, TaskResult result, Dictionary<String, VariableValue> outputs, IList<StepResult> stepResults, CancellationToken token);
|
||||
Task CompleteJobAsync(
|
||||
Guid planId,
|
||||
Guid jobId,
|
||||
TaskResult result,
|
||||
Dictionary<String, VariableValue> outputs,
|
||||
IList<StepResult> stepResults,
|
||||
IList<Annotation> jobAnnotations,
|
||||
CancellationToken token);
|
||||
|
||||
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
||||
}
|
||||
@@ -56,11 +63,18 @@ namespace GitHub.Runner.Common
|
||||
shouldRetry: ex => ex is not TaskOrchestrationJobAlreadyAcquiredException);
|
||||
}
|
||||
|
||||
public Task CompleteJobAsync(Guid planId, Guid jobId, TaskResult result, Dictionary<String, VariableValue> outputs, IList<StepResult> stepResults, CancellationToken cancellationToken)
|
||||
public Task CompleteJobAsync(
|
||||
Guid planId,
|
||||
Guid jobId,
|
||||
TaskResult result,
|
||||
Dictionary<String, VariableValue> outputs,
|
||||
IList<StepResult> stepResults,
|
||||
IList<Annotation> jobAnnotations,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection();
|
||||
return RetryRequest(
|
||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, cancellationToken), cancellationToken);
|
||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken cancellationToken)
|
||||
|
||||
@@ -15,6 +15,7 @@ using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.WebApi;
|
||||
using GitHub.Services.WebApi.Jwt;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Listener
|
||||
@@ -372,6 +373,8 @@ namespace GitHub.Runner.Listener
|
||||
TaskCompletionSource<int> firstJobRequestRenewed = new();
|
||||
var notification = HostContext.GetService<IJobNotification>();
|
||||
|
||||
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// lock renew cancellation token.
|
||||
using (var lockRenewalTokenSource = new CancellationTokenSource())
|
||||
using (var workerProcessCancelTokenSource = new CancellationTokenSource())
|
||||
@@ -379,8 +382,6 @@ namespace GitHub.Runner.Listener
|
||||
long requestId = message.RequestId;
|
||||
Guid lockToken = Guid.Empty; // lockToken has never been used, keep this here of compat
|
||||
|
||||
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// start renew job request
|
||||
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
||||
Task renewJobRequest = RenewJobRequestAsync(message, systemConnection, _poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
||||
@@ -405,7 +406,7 @@ namespace GitHub.Runner.Listener
|
||||
await renewJobRequest;
|
||||
|
||||
// complete job request with result Cancelled
|
||||
await CompleteJobRequestAsync(_poolId, message, lockToken, TaskResult.Canceled);
|
||||
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -544,7 +545,6 @@ namespace GitHub.Runner.Listener
|
||||
detailInfo = string.Join(Environment.NewLine, workerOutput);
|
||||
Trace.Info($"Return code {returnCode} indicate worker encounter an unhandled exception or app crash, attach worker stdout/stderr to JobRequest result.");
|
||||
|
||||
|
||||
var jobServer = await InitializeJobServerAsync(systemConnection);
|
||||
await LogWorkerProcessUnhandledException(jobServer, message, detailInfo);
|
||||
|
||||
@@ -552,7 +552,7 @@ namespace GitHub.Runner.Listener
|
||||
if (detailInfo.Contains(typeof(System.IO.IOException).ToString(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Trace.Info($"Finish job with result 'Failed' due to IOException.");
|
||||
await ForceFailJob(jobServer, message);
|
||||
await ForceFailJob(jobServer, message, detailInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,7 +567,7 @@ namespace GitHub.Runner.Listener
|
||||
await renewJobRequest;
|
||||
|
||||
// complete job request
|
||||
await CompleteJobRequestAsync(_poolId, message, lockToken, result, detailInfo);
|
||||
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, result, detailInfo);
|
||||
|
||||
// print out unhandled exception happened in worker after we complete job request.
|
||||
// when we run out of disk space, report back to server has higher priority.
|
||||
@@ -664,7 +664,7 @@ namespace GitHub.Runner.Listener
|
||||
await renewJobRequest;
|
||||
|
||||
// complete job request
|
||||
await CompleteJobRequestAsync(_poolId, message, lockToken, resultOnAbandonOrCancel);
|
||||
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1065,7 +1065,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
|
||||
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, ServiceEndpoint systemConnection, Guid lockToken, TaskResult result, string detailInfo = null)
|
||||
{
|
||||
Trace.Entering();
|
||||
|
||||
@@ -1077,7 +1077,23 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
if (this._isRunServiceJob)
|
||||
{
|
||||
Trace.Verbose($"Skip FinishAgentRequest call from Listener because MessageType is {message.MessageType}");
|
||||
var runServer = await GetRunServerAsync(systemConnection);
|
||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
|
||||
var unhandledAnnotation = unhandledExceptionIssue.ToAnnotation();
|
||||
var jobAnnotations = new List<Annotation>();
|
||||
if (unhandledAnnotation.HasValue)
|
||||
{
|
||||
jobAnnotations.Add(unhandledAnnotation.Value);
|
||||
}
|
||||
try
|
||||
{
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to raise job completion back to service.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1117,7 +1133,7 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
|
||||
// log an error issue to job level timeline record
|
||||
private async Task LogWorkerProcessUnhandledException(IRunnerService server, Pipelines.AgentJobRequestMessage message, string errorMessage)
|
||||
private async Task LogWorkerProcessUnhandledException(IRunnerService server, Pipelines.AgentJobRequestMessage message, string detailInfo)
|
||||
{
|
||||
if (server is IJobServer jobServer)
|
||||
{
|
||||
@@ -1129,34 +1145,11 @@ namespace GitHub.Runner.Listener
|
||||
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
||||
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(errorMessage) &&
|
||||
message.Variables.TryGetValue("DistributedTask.EnableRunnerIPCDebug", out var enableRunnerIPCDebug) &&
|
||||
StringUtil.ConvertToBoolean(enableRunnerIPCDebug.Value))
|
||||
{
|
||||
// the trace should be best effort and not affect any job result
|
||||
var match = _invalidJsonRegex.Match(errorMessage);
|
||||
if (match.Success &&
|
||||
match.Groups.Count == 2)
|
||||
{
|
||||
var jsonPosition = int.Parse(match.Groups[1].Value);
|
||||
var serializedJobMessage = JsonUtility.ToString(message);
|
||||
var originalJson = serializedJobMessage.Substring(jsonPosition - 10, 20);
|
||||
errorMessage = $"Runner sent Json at position '{jsonPosition}': {originalJson} ({Convert.ToBase64String(Encoding.UTF8.GetBytes(originalJson))})\n{errorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error(ex);
|
||||
errorMessage = $"Fail to check json IPC error: {ex.Message}\n{errorMessage}";
|
||||
}
|
||||
|
||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
|
||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
|
||||
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
||||
jobRecord.ErrorCount++;
|
||||
jobRecord.Issues.Add(unhandledExceptionIssue);
|
||||
|
||||
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1167,13 +1160,13 @@ namespace GitHub.Runner.Listener
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info("Job server does not support handling unhandled exception yet, error message: {0}", errorMessage);
|
||||
Trace.Info("Job server does not support handling unhandled exception yet, error message: {0}", detailInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// raise job completed event to fail the job.
|
||||
private async Task ForceFailJob(IRunnerService server, Pipelines.AgentJobRequestMessage message)
|
||||
private async Task ForceFailJob(IRunnerService server, Pipelines.AgentJobRequestMessage message, string detailInfo)
|
||||
{
|
||||
if (server is IJobServer jobServer)
|
||||
{
|
||||
@@ -1192,7 +1185,15 @@ namespace GitHub.Runner.Listener
|
||||
{
|
||||
try
|
||||
{
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, CancellationToken.None);
|
||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
|
||||
var unhandledAnnotation = unhandledExceptionIssue.ToAnnotation();
|
||||
var jobAnnotations = new List<Annotation>();
|
||||
if (unhandledAnnotation.HasValue)
|
||||
{
|
||||
jobAnnotations.Add(unhandledAnnotation.Value);
|
||||
}
|
||||
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
using Newtonsoft.Json;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
@@ -438,14 +439,26 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
PublishStepTelemetry();
|
||||
|
||||
var stepResult = new StepResult();
|
||||
stepResult.ExternalID = _record.Id;
|
||||
stepResult.Conclusion = _record.Result ?? TaskResult.Succeeded;
|
||||
stepResult.Status = _record.State;
|
||||
stepResult.Number = _record.Order;
|
||||
stepResult.Name = _record.Name;
|
||||
stepResult.StartedAt = _record.StartTime;
|
||||
stepResult.CompletedAt = _record.FinishTime;
|
||||
var stepResult = new StepResult
|
||||
{
|
||||
ExternalID = _record.Id,
|
||||
Conclusion = _record.Result ?? TaskResult.Succeeded,
|
||||
Status = _record.State,
|
||||
Number = _record.Order,
|
||||
Name = _record.Name,
|
||||
StartedAt = _record.StartTime,
|
||||
CompletedAt = _record.FinishTime,
|
||||
Annotations = new List<Annotation>()
|
||||
};
|
||||
|
||||
_record.Issues?.ForEach(issue =>
|
||||
{
|
||||
var annotation = issue.ToAnnotation();
|
||||
if (annotation != null)
|
||||
{
|
||||
stepResult.Annotations.Add(annotation.Value);
|
||||
}
|
||||
});
|
||||
|
||||
Global.StepsResult.Add(stepResult);
|
||||
|
||||
@@ -725,6 +738,9 @@ namespace GitHub.Runner.Worker
|
||||
// Steps results for entire job
|
||||
Global.StepsResult = new List<StepResult>();
|
||||
|
||||
// Job level annotations
|
||||
Global.JobAnnotations = new List<Annotation>();
|
||||
|
||||
// Job Outputs
|
||||
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.Actions.RunService.WebApi;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
@@ -18,6 +19,7 @@ namespace GitHub.Runner.Worker
|
||||
public IDictionary<String, IDictionary<String, String>> JobDefaults { get; set; }
|
||||
public List<ActionsStepTelemetry> StepsTelemetry { get; set; }
|
||||
public List<StepResult> StepsResult { get; set; }
|
||||
public List<Annotation> JobAnnotations { get; set; }
|
||||
public List<JobTelemetry> JobTelemetry { get; set; }
|
||||
public TaskOrchestrationPlanReference Plan { get; set; }
|
||||
public List<string> PrependPath { get; set; }
|
||||
|
||||
@@ -272,7 +272,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
try
|
||||
{
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, default);
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, default);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
35
src/Sdk/RSWebApi/Contracts/Annotation.cs
Normal file
35
src/Sdk/RSWebApi/Contracts/Annotation.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Sdk.RSWebApi.Contracts
|
||||
{
|
||||
[DataContract]
|
||||
public struct Annotation
|
||||
{
|
||||
[DataMember(Name = "level", EmitDefaultValue = false)]
|
||||
public AnnotationLevel Level;
|
||||
|
||||
[DataMember(Name = "message", EmitDefaultValue = false)]
|
||||
public string Message;
|
||||
|
||||
[DataMember(Name = "rawDetails", EmitDefaultValue = false)]
|
||||
public string RawDetails;
|
||||
|
||||
[DataMember(Name = "path", EmitDefaultValue = false)]
|
||||
public string Path;
|
||||
|
||||
[DataMember(Name = "isInfrastructureIssue", EmitDefaultValue = false)]
|
||||
public bool IsInfrastructureIssue;
|
||||
|
||||
[DataMember(Name = "startLine", EmitDefaultValue = false)]
|
||||
public long StartLine;
|
||||
|
||||
[DataMember(Name = "endLine", EmitDefaultValue = false)]
|
||||
public long EndLine;
|
||||
|
||||
[DataMember(Name = "startColumn", EmitDefaultValue = false)]
|
||||
public long StartColumn;
|
||||
|
||||
[DataMember(Name = "endColumn", EmitDefaultValue = false)]
|
||||
public long EndColumn;
|
||||
}
|
||||
}
|
||||
20
src/Sdk/RSWebApi/Contracts/AnnotationLevel.cs
Normal file
20
src/Sdk/RSWebApi/Contracts/AnnotationLevel.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Sdk.RSWebApi.Contracts
|
||||
{
|
||||
[DataContract]
|
||||
public enum AnnotationLevel
|
||||
{
|
||||
[EnumMember]
|
||||
UNKNOWN = 0,
|
||||
|
||||
[EnumMember]
|
||||
NOTICE = 1,
|
||||
|
||||
[EnumMember]
|
||||
WARNING = 2,
|
||||
|
||||
[EnumMember]
|
||||
FAILURE = 3
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
|
||||
namespace GitHub.Actions.RunService.WebApi
|
||||
{
|
||||
@@ -22,5 +23,8 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
|
||||
[DataMember(Name = "stepResults", EmitDefaultValue = false)]
|
||||
public IList<StepResult> StepResults { get; set; }
|
||||
|
||||
[DataMember(Name = "annotations", EmitDefaultValue = false)]
|
||||
public IList<Annotation> Annotations { get; set; }
|
||||
}
|
||||
}
|
||||
91
src/Sdk/RSWebApi/Contracts/IssueExtensions.cs
Normal file
91
src/Sdk/RSWebApi/Contracts/IssueExtensions.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace Sdk.RSWebApi.Contracts
|
||||
{
|
||||
public static class IssueExtensions
|
||||
{
|
||||
public static Annotation? ToAnnotation(this Issue issue)
|
||||
{
|
||||
var issueMessage = issue.Message;
|
||||
if (string.IsNullOrWhiteSpace(issueMessage))
|
||||
{
|
||||
if (!issue.Data.TryGetValue(RunIssueKeys.Message, out issueMessage) || string.IsNullOrWhiteSpace(issueMessage))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var annotationLevel = GetAnnotationLevel(issue.Type);
|
||||
var path = GetFilePath(issue);
|
||||
var lineNumber = GetAnnotationNumber(issue, RunIssueKeys.Line) ?? 0;
|
||||
var endLineNumber = GetAnnotationNumber(issue, RunIssueKeys.EndLine) ?? lineNumber;
|
||||
var columnNumber = GetAnnotationNumber(issue, RunIssueKeys.Col) ?? 0;
|
||||
var endColumnNumber = GetAnnotationNumber(issue, RunIssueKeys.EndColumn) ?? columnNumber;
|
||||
var logLineNumber = GetAnnotationNumber(issue, RunIssueKeys.LogLineNumber) ?? 0;
|
||||
|
||||
if (path == null && lineNumber == 0 && logLineNumber != 0)
|
||||
{
|
||||
lineNumber = logLineNumber;
|
||||
endLineNumber = logLineNumber;
|
||||
}
|
||||
|
||||
return new Annotation
|
||||
{
|
||||
Level = annotationLevel,
|
||||
Message = issueMessage,
|
||||
Path = path,
|
||||
StartLine = lineNumber,
|
||||
EndLine = endLineNumber,
|
||||
StartColumn = columnNumber,
|
||||
EndColumn = endColumnNumber,
|
||||
};
|
||||
}
|
||||
|
||||
private static AnnotationLevel GetAnnotationLevel(IssueType issueType)
|
||||
{
|
||||
switch (issueType)
|
||||
{
|
||||
case IssueType.Error:
|
||||
return AnnotationLevel.FAILURE;
|
||||
case IssueType.Warning:
|
||||
return AnnotationLevel.WARNING;
|
||||
case IssueType.Notice:
|
||||
return AnnotationLevel.NOTICE;
|
||||
default:
|
||||
return AnnotationLevel.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private static int? GetAnnotationNumber(Issue issue, string key)
|
||||
{
|
||||
if (issue.Data.TryGetValue(key, out var numberString) &&
|
||||
int.TryParse(numberString, out var number))
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetAnnotationField(Issue issue, string key)
|
||||
{
|
||||
if (issue.Data.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetFilePath(Issue issue)
|
||||
{
|
||||
if (issue.Data.TryGetValue(RunIssueKeys.File, out var path) &&
|
||||
!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Sdk/RSWebApi/Contracts/IssueKeys.cs
Normal file
13
src/Sdk/RSWebApi/Contracts/IssueKeys.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Sdk.RSWebApi.Contracts
|
||||
{
|
||||
public static class RunIssueKeys
|
||||
{
|
||||
public const string Message = "message";
|
||||
public const string File = "file";
|
||||
public const string Line = "line";
|
||||
public const string Col = "col";
|
||||
public const string EndLine = "endLine";
|
||||
public const string EndColumn = "endColumn";
|
||||
public const string LogLineNumber = "logFileLineNumber";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
|
||||
namespace GitHub.Actions.RunService.WebApi
|
||||
{
|
||||
@@ -34,5 +36,8 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
|
||||
[DataMember(Name = "completed_log_lines", EmitDefaultValue = false)]
|
||||
public long? CompletedLogLines { get; set; }
|
||||
|
||||
[DataMember(Name = "annotations", EmitDefaultValue = false)]
|
||||
public List<Annotation> Annotations { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,7 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
TaskResult result,
|
||||
Dictionary<String, VariableValue> outputs,
|
||||
IList<StepResult> stepResults,
|
||||
IList<Annotation> jobAnnotations,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpMethod httpMethod = new HttpMethod("POST");
|
||||
@@ -109,7 +110,8 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
JobID = jobId,
|
||||
Conclusion = result,
|
||||
Outputs = outputs,
|
||||
StepResults = stepResults
|
||||
StepResults = stepResults,
|
||||
Annotations = jobAnnotations
|
||||
};
|
||||
|
||||
requestUri = new Uri(requestUri, "completejob");
|
||||
|
||||
70
src/Test/L0/Sdk/RSWebApi/AnnotationsL0.cs
Normal file
70
src/Test/L0/Sdk/RSWebApi/AnnotationsL0.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Actions.RunService.WebApi.Tests;
|
||||
|
||||
public sealed class AnnotationsL0
|
||||
{
|
||||
[Fact]
|
||||
public void ToAnnotation_ValidIssueWithMessage_ReturnsAnnotation()
|
||||
{
|
||||
var issue = new Issue
|
||||
{
|
||||
Type = IssueType.Error,
|
||||
Message = "An error occurred",
|
||||
IsInfrastructureIssue = true
|
||||
};
|
||||
|
||||
issue.Data.Add(RunIssueKeys.File, "test.txt");
|
||||
issue.Data.Add(RunIssueKeys.Line, "5");
|
||||
issue.Data.Add(RunIssueKeys.Col, "10");
|
||||
issue.Data.Add(RunIssueKeys.EndLine, "8");
|
||||
issue.Data.Add(RunIssueKeys.EndColumn, "20");
|
||||
issue.Data.Add(RunIssueKeys.LogLineNumber, "2");
|
||||
|
||||
var annotation = issue.ToAnnotation();
|
||||
|
||||
Assert.NotNull(annotation);
|
||||
Assert.Equal(AnnotationLevel.FAILURE, annotation.Value.Level);
|
||||
Assert.Equal("An error occurred", annotation.Value.Message);
|
||||
Assert.Equal("test.txt", annotation.Value.Path);
|
||||
Assert.Equal(5, annotation.Value.StartLine);
|
||||
Assert.Equal(8, annotation.Value.EndLine);
|
||||
Assert.Equal(10, annotation.Value.StartColumn);
|
||||
Assert.Equal(20, annotation.Value.EndColumn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToAnnotation_ValidIssueWithEmptyMessage_ReturnsNull()
|
||||
{
|
||||
var issue = new Issue
|
||||
{
|
||||
Type = IssueType.Warning,
|
||||
Message = string.Empty
|
||||
};
|
||||
|
||||
var annotation = issue.ToAnnotation();
|
||||
|
||||
Assert.Null(annotation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToAnnotation_ValidIssueWithMessageInData_ReturnsAnnotation()
|
||||
{
|
||||
var issue = new Issue
|
||||
{
|
||||
Type = IssueType.Warning,
|
||||
Message = string.Empty,
|
||||
};
|
||||
|
||||
issue.Data.Add(RunIssueKeys.Message, "A warning occurred");
|
||||
|
||||
var annotation = issue.ToAnnotation();
|
||||
|
||||
Assert.NotNull(annotation);
|
||||
Assert.Equal(AnnotationLevel.WARNING, annotation.Value.Level);
|
||||
Assert.Equal("A warning occurred", annotation.Value.Message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user