mirror of
https://github.com/actions/runner.git
synced 2025-12-11 04:46:58 +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<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);
|
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
||||||
}
|
}
|
||||||
@@ -56,11 +63,18 @@ namespace GitHub.Runner.Common
|
|||||||
shouldRetry: ex => ex is not TaskOrchestrationJobAlreadyAcquiredException);
|
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();
|
CheckConnection();
|
||||||
return RetryRequest(
|
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)
|
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.Common;
|
||||||
using GitHub.Services.WebApi;
|
using GitHub.Services.WebApi;
|
||||||
using GitHub.Services.WebApi.Jwt;
|
using GitHub.Services.WebApi.Jwt;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
namespace GitHub.Runner.Listener
|
namespace GitHub.Runner.Listener
|
||||||
@@ -372,6 +373,8 @@ namespace GitHub.Runner.Listener
|
|||||||
TaskCompletionSource<int> firstJobRequestRenewed = new();
|
TaskCompletionSource<int> firstJobRequestRenewed = new();
|
||||||
var notification = HostContext.GetService<IJobNotification>();
|
var notification = HostContext.GetService<IJobNotification>();
|
||||||
|
|
||||||
|
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
// lock renew cancellation token.
|
// lock renew cancellation token.
|
||||||
using (var lockRenewalTokenSource = new CancellationTokenSource())
|
using (var lockRenewalTokenSource = new CancellationTokenSource())
|
||||||
using (var workerProcessCancelTokenSource = new CancellationTokenSource())
|
using (var workerProcessCancelTokenSource = new CancellationTokenSource())
|
||||||
@@ -379,8 +382,6 @@ namespace GitHub.Runner.Listener
|
|||||||
long requestId = message.RequestId;
|
long requestId = message.RequestId;
|
||||||
Guid lockToken = Guid.Empty; // lockToken has never been used, keep this here of compat
|
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
|
// start renew job request
|
||||||
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
Trace.Info($"Start renew job request {requestId} for job {message.JobId}.");
|
||||||
Task renewJobRequest = RenewJobRequestAsync(message, systemConnection, _poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
Task renewJobRequest = RenewJobRequestAsync(message, systemConnection, _poolId, requestId, lockToken, orchestrationId, firstJobRequestRenewed, lockRenewalTokenSource.Token);
|
||||||
@@ -405,7 +406,7 @@ namespace GitHub.Runner.Listener
|
|||||||
await renewJobRequest;
|
await renewJobRequest;
|
||||||
|
|
||||||
// complete job request with result Cancelled
|
// complete job request with result Cancelled
|
||||||
await CompleteJobRequestAsync(_poolId, message, lockToken, TaskResult.Canceled);
|
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +545,6 @@ namespace GitHub.Runner.Listener
|
|||||||
detailInfo = string.Join(Environment.NewLine, workerOutput);
|
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.");
|
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);
|
var jobServer = await InitializeJobServerAsync(systemConnection);
|
||||||
await LogWorkerProcessUnhandledException(jobServer, message, detailInfo);
|
await LogWorkerProcessUnhandledException(jobServer, message, detailInfo);
|
||||||
|
|
||||||
@@ -552,7 +552,7 @@ namespace GitHub.Runner.Listener
|
|||||||
if (detailInfo.Contains(typeof(System.IO.IOException).ToString(), StringComparison.OrdinalIgnoreCase))
|
if (detailInfo.Contains(typeof(System.IO.IOException).ToString(), StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
Trace.Info($"Finish job with result 'Failed' due to IOException.");
|
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;
|
await renewJobRequest;
|
||||||
|
|
||||||
// complete job request
|
// 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.
|
// 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.
|
// when we run out of disk space, report back to server has higher priority.
|
||||||
@@ -664,7 +664,7 @@ namespace GitHub.Runner.Listener
|
|||||||
await renewJobRequest;
|
await renewJobRequest;
|
||||||
|
|
||||||
// complete job request
|
// complete job request
|
||||||
await CompleteJobRequestAsync(_poolId, message, lockToken, resultOnAbandonOrCancel);
|
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
|
||||||
}
|
}
|
||||||
finally
|
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();
|
Trace.Entering();
|
||||||
|
|
||||||
@@ -1077,7 +1077,23 @@ namespace GitHub.Runner.Listener
|
|||||||
|
|
||||||
if (this._isRunServiceJob)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,7 +1133,7 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
|
|
||||||
// log an error issue to job level timeline record
|
// 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)
|
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");
|
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
|
||||||
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
|
||||||
|
|
||||||
try
|
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
|
||||||
{
|
|
||||||
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 };
|
|
||||||
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
|
||||||
jobRecord.ErrorCount++;
|
jobRecord.ErrorCount++;
|
||||||
jobRecord.Issues.Add(unhandledExceptionIssue);
|
jobRecord.Issues.Add(unhandledExceptionIssue);
|
||||||
|
|
||||||
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -1167,13 +1160,13 @@ namespace GitHub.Runner.Listener
|
|||||||
}
|
}
|
||||||
else
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// raise job completed event to fail the job.
|
// 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)
|
if (server is IJobServer jobServer)
|
||||||
{
|
{
|
||||||
@@ -1192,7 +1185,15 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
try
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using GitHub.Runner.Sdk;
|
|||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using GitHub.Runner.Worker.Handlers;
|
using GitHub.Runner.Worker.Handlers;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||||
|
|
||||||
@@ -438,14 +439,26 @@ namespace GitHub.Runner.Worker
|
|||||||
|
|
||||||
PublishStepTelemetry();
|
PublishStepTelemetry();
|
||||||
|
|
||||||
var stepResult = new StepResult();
|
var stepResult = new StepResult
|
||||||
stepResult.ExternalID = _record.Id;
|
{
|
||||||
stepResult.Conclusion = _record.Result ?? TaskResult.Succeeded;
|
ExternalID = _record.Id,
|
||||||
stepResult.Status = _record.State;
|
Conclusion = _record.Result ?? TaskResult.Succeeded,
|
||||||
stepResult.Number = _record.Order;
|
Status = _record.State,
|
||||||
stepResult.Name = _record.Name;
|
Number = _record.Order,
|
||||||
stepResult.StartedAt = _record.StartTime;
|
Name = _record.Name,
|
||||||
stepResult.CompletedAt = _record.FinishTime;
|
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);
|
Global.StepsResult.Add(stepResult);
|
||||||
|
|
||||||
@@ -725,6 +738,9 @@ namespace GitHub.Runner.Worker
|
|||||||
// Steps results for entire job
|
// Steps results for entire job
|
||||||
Global.StepsResult = new List<StepResult>();
|
Global.StepsResult = new List<StepResult>();
|
||||||
|
|
||||||
|
// Job level annotations
|
||||||
|
Global.JobAnnotations = new List<Annotation>();
|
||||||
|
|
||||||
// Job Outputs
|
// Job Outputs
|
||||||
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GitHub.Actions.RunService.WebApi;
|
using GitHub.Actions.RunService.WebApi;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Worker.Container;
|
using GitHub.Runner.Worker.Container;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
|
|
||||||
namespace GitHub.Runner.Worker
|
namespace GitHub.Runner.Worker
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,7 @@ namespace GitHub.Runner.Worker
|
|||||||
public IDictionary<String, IDictionary<String, String>> JobDefaults { get; set; }
|
public IDictionary<String, IDictionary<String, String>> JobDefaults { get; set; }
|
||||||
public List<ActionsStepTelemetry> StepsTelemetry { get; set; }
|
public List<ActionsStepTelemetry> StepsTelemetry { get; set; }
|
||||||
public List<StepResult> StepsResult { get; set; }
|
public List<StepResult> StepsResult { get; set; }
|
||||||
|
public List<Annotation> JobAnnotations { get; set; }
|
||||||
public List<JobTelemetry> JobTelemetry { get; set; }
|
public List<JobTelemetry> JobTelemetry { get; set; }
|
||||||
public TaskOrchestrationPlanReference Plan { get; set; }
|
public TaskOrchestrationPlanReference Plan { get; set; }
|
||||||
public List<string> PrependPath { get; set; }
|
public List<string> PrependPath { get; set; }
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ namespace GitHub.Runner.Worker
|
|||||||
{
|
{
|
||||||
try
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
|
|
||||||
namespace GitHub.Actions.RunService.WebApi
|
namespace GitHub.Actions.RunService.WebApi
|
||||||
{
|
{
|
||||||
@@ -22,5 +23,8 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
|
|
||||||
[DataMember(Name = "stepResults", EmitDefaultValue = false)]
|
[DataMember(Name = "stepResults", EmitDefaultValue = false)]
|
||||||
public IList<StepResult> StepResults { get; set; }
|
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;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
using Sdk.RSWebApi.Contracts;
|
||||||
|
|
||||||
namespace GitHub.Actions.RunService.WebApi
|
namespace GitHub.Actions.RunService.WebApi
|
||||||
{
|
{
|
||||||
@@ -34,5 +36,8 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
|
|
||||||
[DataMember(Name = "completed_log_lines", EmitDefaultValue = false)]
|
[DataMember(Name = "completed_log_lines", EmitDefaultValue = false)]
|
||||||
public long? CompletedLogLines { get; set; }
|
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,
|
TaskResult result,
|
||||||
Dictionary<String, VariableValue> outputs,
|
Dictionary<String, VariableValue> outputs,
|
||||||
IList<StepResult> stepResults,
|
IList<StepResult> stepResults,
|
||||||
|
IList<Annotation> jobAnnotations,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
HttpMethod httpMethod = new HttpMethod("POST");
|
HttpMethod httpMethod = new HttpMethod("POST");
|
||||||
@@ -109,7 +110,8 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
JobID = jobId,
|
JobID = jobId,
|
||||||
Conclusion = result,
|
Conclusion = result,
|
||||||
Outputs = outputs,
|
Outputs = outputs,
|
||||||
StepResults = stepResults
|
StepResults = stepResults,
|
||||||
|
Annotations = jobAnnotations
|
||||||
};
|
};
|
||||||
|
|
||||||
requestUri = new Uri(requestUri, "completejob");
|
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