Compare commits

..

6 Commits

Author SHA1 Message Date
John Wesley Walker III
290e9248bf Removed BOM from modified files as demanded by PR linter. 2023-05-02 16:46:21 +00:00
John Wesley Walker III
75ffe93f62 When inferring additional secrets from multi-line content, ignore very short lines. 2023-05-02 16:32:09 +00:00
John Wesley Walker III
afcca9bfa4 Allow public SecretMasker constructor to be called with a null IEnumerable<ValueEncoder> 2023-04-06 10:46:35 +02:00
John Wesley Walker III
157e03616e Set file encodings back to UTF8 (no-BOM) 2023-04-05 12:04:48 +00:00
John Wesley Walker III
30f686b9c2 Added a unit test for thorough Base64 secret masking. 2023-04-05 11:43:26 +00:00
John Wesley Walker III
ec5d72810f Omit Base64 padding characters from Base64 representations of secrets. 2023-04-05 08:27:23 +00:00
74 changed files with 716 additions and 2705 deletions

View File

@@ -157,7 +157,7 @@ cat (Runner/Worker)_TIMESTAMP.log # view your log file
## Styling
We use the .NET Foundation and CoreCLR style guidelines [located here](
https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md)
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md)
### Format C# Code
@@ -165,4 +165,4 @@ To format both staged and unstaged .cs files
```
cd ./src
./dev.(cmd|sh) format
```
```

View File

@@ -2,7 +2,7 @@ FROM mcr.microsoft.com/dotnet/runtime-deps:6.0 as build
ARG RUNNER_VERSION
ARG RUNNER_ARCH="x64"
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.3.2
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.3.1
ARG DOCKER_VERSION=20.10.23
RUN apt update -y && apt install curl unzip -y

View File

@@ -1,18 +1,17 @@
## Features
- Runner changes for communication with Results service (#2510, #2531, #2535, #2516)
- Add `*.ghe.localhost` domains to hosted server check (#2536)
- Add `OrchestrationId` to user-agent for better telemetry correlation. (#2568)
- Support matrix context in output keys (#2477)
- Add update certificates to `./run.sh` if `RUNNER_UPDATE_CA_CERTS` env is set (#2471)
- Bypass all proxies for all hosts if `no_proxy='*'` is set (#2395)
- Change runner image to make user/folder align with `ubuntu-latest` hosted runner. (#2469)
## Bugs
- Fix JIT configurations on Windows (#2497)
- Guard against NullReference while creating HostContext (#2343)
- Handles broken symlink in `Which` (#2150, #2196)
- Adding curl retry for external tool downloads (#2552, #2557)
- Limit the time we wait for waiting websocket to connect. (#2554)
- Exit on runner version deprecation error (#2299)
- Runner service exit after consecutive re-try exits (#2426)
## Misc
- Bump container hooks version to 0.3.1 in runner image (#2496)
- Runner changes to communicate with vNext services (#2487, #2500, #2505, #2541, #2547)
- Replace deprecated command with environment file (#2429)
- Make requests to `Run` service to renew job request (#2461)
- Add job/step log upload to Result service (#2447, #2439)
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.

View File

@@ -55,23 +55,12 @@ function acquireExternalTool() {
# Download from source to the partial file.
echo "Downloading $download_source"
mkdir -p "$(dirname "$download_target")" || checkRC 'mkdir'
CURL_VERSION=$(curl --version | awk 'NR==1{print $2}')
echo "Curl version: $CURL_VERSION"
# curl -f Fail silently (no output at all) on HTTP errors (H)
# -k Allow connections to SSL sites without certs (H)
# -S Show error. With -s, make curl show errors when they occur
# -L Follow redirects (H)
# -o FILE Write to FILE instead of stdout
# --retry 3 Retries transient errors 3 times (timeouts, 5xx)
if [[ "$(printf '%s\n' "7.71.0" "$CURL_VERSION" | sort -V | head -n1)" != "7.71.0" ]]; then
# Curl version is less than or equal to 7.71.0, skipping retry-all-errors flag
curl -fkSL --retry 3 -o "$partial_target" "$download_source" 2>"${download_target}_download.log" || checkRC 'curl'
else
# Curl version is greater than 7.71.0, running curl with --retry-all-errors flag
curl -fkSL --retry 3 --retry-all-errors -o "$partial_target" "$download_source" 2>"${download_target}_download.log" || checkRC 'curl'
fi
curl -fkSL -o "$partial_target" "$download_source" 2>"${download_target}_download.log" || checkRC 'curl'
# Move the partial file to the download target.
mv "$partial_target" "$download_target" || checkRC 'mv'

View File

@@ -1,4 +1,4 @@
using System;
using System;
namespace GitHub.Runner.Common
{
@@ -132,7 +132,6 @@ namespace GitHub.Runner.Common
public static readonly string GenerateServiceConfig = "generateServiceConfig";
public static readonly string Help = "help";
public static readonly string Local = "local";
public static readonly string NoDefaultLabels = "no-default-labels";
public static readonly string Replace = "replace";
public static readonly string DisableUpdate = "disableupdate";
public static readonly string Once = "once"; // Keep this around since customers still relies on it
@@ -155,7 +154,6 @@ namespace GitHub.Runner.Common
{
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
public static readonly string Node12Warning = "DistributedTask.AddWarningToNode12Action";
public static readonly string LogTemplateErrorsToTraceWriter = "DistributedTask.LogTemplateErrorsToTraceWriter";
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
}
@@ -263,8 +261,6 @@ namespace GitHub.Runner.Common
public static readonly string AccessToken = "system.accessToken";
public static readonly string Culture = "system.culture";
public static readonly string PhaseDisplayName = "system.phaseDisplayName";
public static readonly string JobRequestType = "system.jobRequestType";
public static readonly string OrchestrationId = "system.orchestrationId";
}
}

View File

@@ -53,7 +53,7 @@ namespace GitHub.Runner.Common
private static int[] _vssHttpCredentialEventIds = new int[] { 11, 13, 14, 15, 16, 17, 18, 20, 21, 22, 27, 29 };
private readonly ConcurrentDictionary<Type, object> _serviceInstances = new();
private readonly ConcurrentDictionary<Type, Type> _serviceTypes = new();
private readonly ISecretMasker _secretMasker = new SecretMasker();
private readonly ISecretMasker _secretMasker;
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
private CancellationTokenSource _runnerShutdownTokenSource = new();
private object _perfLock = new();
@@ -82,17 +82,20 @@ namespace GitHub.Runner.Common
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
_loadContext.Unloading += LoadContext_Unloading;
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift1);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift2);
this.SecretMasker.AddValueEncoder(ValueEncoders.CommandLineArgumentEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.ExpressionStringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.TrimDoubleQuotes);
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPreAmpersandEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPostAmpersandEscape);
var masks = new List<ValueEncoder>()
{
ValueEncoders.EnumerateBase64Variations,
ValueEncoders.CommandLineArgumentEscape,
ValueEncoders.ExpressionStringEscape,
ValueEncoders.JsonStringEscape,
ValueEncoders.UriDataEscape,
ValueEncoders.XmlDataEscape,
ValueEncoders.TrimDoubleQuotes,
ValueEncoders.PowerShellPreAmpersandEscape,
ValueEncoders.PowerShellPostAmpersandEscape
};
_secretMasker = new SecretMasker(masks);
// Create StdoutTraceListener if ENV is set
StdoutTraceListener stdoutTraceListener = null;

View File

@@ -11,10 +11,10 @@ using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Sdk;
using GitHub.Services.Common;
using GitHub.Services.OAuth;
using GitHub.Services.Results.Client;
using GitHub.Services.WebApi;
using GitHub.Services.WebApi.Utilities.Internal;
using GitHub.Services.Results.Client;
using GitHub.Services.OAuth;
namespace GitHub.Runner.Common
{
@@ -199,15 +199,13 @@ namespace GitHub.Runner.Common
{
Trace.Info($"Attempting to start websocket client with delay {delay}.");
await Task.Delay(delay);
using var connectTimeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), connectTimeoutTokenSource.Token);
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), default(CancellationToken));
Trace.Info($"Successfully started websocket client.");
}
catch (Exception ex)
{
Trace.Info("Exception caught during websocket client connect, fallback of HTTP would be used now instead of websocket.");
Trace.Error(ex);
this._websocketClient = null;
}
}
@@ -254,7 +252,7 @@ namespace GitHub.Runner.Common
{
failedAttemptsToPostBatchedLinesByWebsocket++;
Trace.Info($"Caught exception during append web console line to websocket, let's fallback to sending via non-websocket call (total calls: {totalBatchedLinesAttemptedByWebsocket}, failed calls: {failedAttemptsToPostBatchedLinesByWebsocket}, websocket state: {this._websocketClient?.State}).");
Trace.Verbose(ex.ToString());
Trace.Error(ex);
if (totalBatchedLinesAttemptedByWebsocket > _minWebsocketBatchedLinesCountToConsider)
{
// let's consider failure percentage

View File

@@ -17,7 +17,7 @@ namespace GitHub.Runner.Common
TaskCompletionSource<int> JobRecordUpdated { get; }
event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
Task ShutdownAsync();
void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultServiceOnly = false);
void Start(Pipelines.AgentJobRequestMessage jobRequest);
void QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber = null);
void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource);
void QueueResultsUpload(Guid timelineRecordId, string name, string path, string type, bool deleteSource, bool finalize, bool firstBlock, long totalLines);
@@ -70,7 +70,6 @@ namespace GitHub.Runner.Common
private readonly TaskCompletionSource<int> _jobCompletionSource = new();
private readonly TaskCompletionSource<int> _jobRecordUpdated = new();
private bool _queueInProcess = false;
private bool _resultsServiceOnly = false;
public TaskCompletionSource<int> JobRecordUpdated => _jobRecordUpdated;
@@ -96,17 +95,13 @@ namespace GitHub.Runner.Common
_resultsServer = hostContext.GetService<IResultsServer>();
}
public void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultServiceOnly = false)
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
{
Trace.Entering();
_resultsServiceOnly = resultServiceOnly;
var serviceEndPoint = jobRequest.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
if (!resultServiceOnly)
{
_jobServer.InitializeWebsocketClient(serviceEndPoint);
}
_jobServer.InitializeWebsocketClient(serviceEndPoint);
// This code is usually wrapped by an instance of IExecutionContext which isn't available here.
jobRequest.Variables.TryGetValue("system.github.results_endpoint", out VariableValue resultsEndpointVariable);
@@ -117,16 +112,8 @@ namespace GitHub.Runner.Common
!string.IsNullOrEmpty(accessToken) &&
!string.IsNullOrEmpty(resultsReceiverEndpoint))
{
string liveConsoleFeedUrl = null;
Trace.Info("Initializing results client");
if (resultServiceOnly
&& serviceEndPoint.Data.TryGetValue("FeedStreamUrl", out var feedStreamUrl)
&& !string.IsNullOrEmpty(feedStreamUrl))
{
liveConsoleFeedUrl = feedStreamUrl;
}
_resultsServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), liveConsoleFeedUrl, accessToken);
_resultsServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), accessToken);
_resultsClientInitiated = true;
}
@@ -207,9 +194,6 @@ namespace GitHub.Runner.Common
Trace.Info($"Disposing job server ...");
await _jobServer.DisposeAsync();
Trace.Info($"Disposing results server ...");
await _resultsServer.DisposeAsync();
Trace.Info("All queue process tasks have been stopped, and all queues are drained.");
}
@@ -388,14 +372,7 @@ namespace GitHub.Runner.Common
// Give at most 60s for each request.
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
{
if (_resultsServiceOnly)
{
await _resultsServer.AppendLiveConsoleFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), batch[0].LineNumber, timeoutTokenSource.Token);
}
else
{
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), batch[0].LineNumber, timeoutTokenSource.Token);
}
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), batch[0].LineNumber, timeoutTokenSource.Token);
}
if (_firstConsoleOutputs)
@@ -622,7 +599,7 @@ namespace GitHub.Runner.Common
foreach (var detailTimeline in update.PendingRecords.Where(r => r.Details != null))
{
if (!_resultsServiceOnly && !_allTimelines.Contains(detailTimeline.Details.Id))
if (!_allTimelines.Contains(detailTimeline.Details.Id))
{
try
{
@@ -644,11 +621,7 @@ namespace GitHub.Runner.Common
try
{
if (!_resultsServiceOnly)
{
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
}
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
try
{
if (_resultsClientInitiated)
@@ -756,17 +729,17 @@ namespace GitHub.Runner.Common
timelineRecord.State = rec.State ?? timelineRecord.State;
timelineRecord.WorkerName = rec.WorkerName ?? timelineRecord.WorkerName;
if (rec.ErrorCount > 0)
if (rec.ErrorCount != null && rec.ErrorCount > 0)
{
timelineRecord.ErrorCount = rec.ErrorCount;
}
if (rec.WarningCount > 0)
if (rec.WarningCount != null && rec.WarningCount > 0)
{
timelineRecord.WarningCount = rec.WarningCount;
}
if (rec.NoticeCount > 0)
if (rec.NoticeCount != null && rec.NoticeCount > 0)
{
timelineRecord.NoticeCount = rec.NoticeCount;
}
@@ -797,7 +770,7 @@ namespace GitHub.Runner.Common
foreach (var record in mergedRecords)
{
Trace.Verbose($" Record: t={record.RecordType}, n={record.Name}, s={record.State}, st={record.StartTime}, {record.PercentComplete}%, ft={record.FinishTime}, r={record.Result}: {record.CurrentOperation}");
if (record.Issues != null)
if (record.Issues != null && record.Issues.Count > 0)
{
foreach (var issue in record.Issues)
{
@@ -807,7 +780,7 @@ namespace GitHub.Runner.Common
}
}
if (record.Variables != null)
if (record.Variables != null && record.Variables.Count > 0)
{
foreach (var variable in record.Variables)
{
@@ -824,30 +797,27 @@ namespace GitHub.Runner.Common
bool uploadSucceed = false;
try
{
if (!_resultsServiceOnly)
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
{
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
// Create the log
var taskLog = await _jobServer.CreateLogAsync(_scopeIdentifier, _hubName, _planId, new TaskLog(String.Format(@"logs\{0:D}", file.TimelineRecordId)), default(CancellationToken));
// Upload the contents
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
// Create the log
var taskLog = await _jobServer.CreateLogAsync(_scopeIdentifier, _hubName, _planId, new TaskLog(String.Format(@"logs\{0:D}", file.TimelineRecordId)), default(CancellationToken));
// Upload the contents
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
var logUploaded = await _jobServer.AppendLogContentAsync(_scopeIdentifier, _hubName, _planId, taskLog.Id, fs, default(CancellationToken));
}
// Create a new record and only set the Log field
var attachmentUpdataRecord = new TimelineRecord() { Id = file.TimelineRecordId, Log = taskLog };
QueueTimelineRecordUpdate(file.TimelineId, attachmentUpdataRecord);
var logUploaded = await _jobServer.AppendLogContentAsync(_scopeIdentifier, _hubName, _planId, taskLog.Id, fs, default(CancellationToken));
}
else
// Create a new record and only set the Log field
var attachmentUpdataRecord = new TimelineRecord() { Id = file.TimelineRecordId, Log = taskLog };
QueueTimelineRecordUpdate(file.TimelineId, attachmentUpdataRecord);
}
else
{
// Create attachment
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
// Create attachment
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
var result = await _jobServer.CreateAttachmentAsync(_scopeIdentifier, _hubName, _planId, file.TimelineId, file.TimelineRecordId, file.Type, file.Name, fs, default(CancellationToken));
}
var result = await _jobServer.CreateAttachmentAsync(_scopeIdentifier, _hubName, _planId, file.TimelineId, file.TimelineRecordId, file.Type, file.Name, fs, default(CancellationToken));
}
}

View File

@@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Services.Launch.Client;
using GitHub.Services.WebApi;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(LaunchServer))]
public interface ILaunchServer : IRunnerService
{
void InitializeLaunchClient(Uri uri, string token);
Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken);
}
public sealed class LaunchServer : RunnerService, ILaunchServer
{
private LaunchHttpClient _launchClient;
public void InitializeLaunchClient(Uri uri, string token)
{
var httpMessageHandler = HostContext.CreateHttpClientHandler();
this._launchClient = new LaunchHttpClient(uri, httpMessageHandler, token, disposeHandler: true);
}
public Task<ActionDownloadInfoCollection> ResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList,
CancellationToken cancellationToken)
{
if (_launchClient != null)
{
return _launchClient.GetResolveActionsDownloadInfoAsync(planId, jobId, actionReferenceList,
cancellationToken: cancellationToken);
}
throw new InvalidOperationException("Launch client is not initialized.");
}
}
}

View File

@@ -1,26 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Sdk;
using GitHub.Services.Common;
using GitHub.Services.Results.Client;
using GitHub.Services.WebApi.Utilities.Internal;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(ResultServer))]
public interface IResultsServer : IRunnerService, IAsyncDisposable
public interface IResultsServer : IRunnerService
{
void InitializeResultsClient(Uri uri, string liveConsoleFeedUrl, string token);
Task<bool> AppendLiveConsoleFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long? startLine, CancellationToken cancellationToken);
void InitializeResultsClient(Uri uri, string token);
// logging and console
Task CreateResultsStepSummaryAsync(string planId, string jobId, Guid stepId, string file,
@@ -40,26 +31,10 @@ namespace GitHub.Runner.Common
{
private ResultsHttpClient _resultsClient;
private ClientWebSocket _websocketClient;
private DateTime? _lastConnectionFailure;
private static readonly TimeSpan MinDelayForWebsocketReconnect = TimeSpan.FromMilliseconds(100);
private static readonly TimeSpan MaxDelayForWebsocketReconnect = TimeSpan.FromMilliseconds(500);
private Task _websocketConnectTask;
private String _liveConsoleFeedUrl;
private string _token;
public void InitializeResultsClient(Uri uri, string liveConsoleFeedUrl, string token)
public void InitializeResultsClient(Uri uri, string token)
{
var httpMessageHandler = HostContext.CreateHttpClientHandler();
this._resultsClient = new ResultsHttpClient(uri, httpMessageHandler, token, disposeHandler: true);
_token = token;
if (!string.IsNullOrEmpty(liveConsoleFeedUrl))
{
_liveConsoleFeedUrl = liveConsoleFeedUrl;
InitializeWebsocketClient(liveConsoleFeedUrl, token, TimeSpan.Zero, retryConnection: true);
}
}
public Task CreateResultsStepSummaryAsync(string planId, string jobId, Guid stepId, string file,
@@ -119,144 +94,5 @@ namespace GitHub.Runner.Common
throw new InvalidOperationException("Results client is not initialized.");
}
public ValueTask DisposeAsync()
{
CloseWebSocket(WebSocketCloseStatus.NormalClosure, CancellationToken.None);
GC.SuppressFinalize(this);
return ValueTask.CompletedTask;
}
private void InitializeWebsocketClient(string liveConsoleFeedUrl, string accessToken, TimeSpan delay, bool retryConnection = false)
{
if (string.IsNullOrEmpty(accessToken))
{
Trace.Info($"No access token from server");
return;
}
if (string.IsNullOrEmpty(liveConsoleFeedUrl))
{
Trace.Info($"No live console feed url from server");
return;
}
Trace.Info($"Creating websocket client ..." + liveConsoleFeedUrl);
this._websocketClient = new ClientWebSocket();
this._websocketClient.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}");
var userAgentValues = new List<ProductInfoHeaderValue>();
userAgentValues.AddRange(UserAgentUtility.GetDefaultRestUserAgent());
userAgentValues.AddRange(HostContext.UserAgents);
this._websocketClient.Options.SetRequestHeader("User-Agent", string.Join(" ", userAgentValues.Select(x => x.ToString())));
// during initialization, retry upto 3 times to setup connection
this._websocketConnectTask = ConnectWebSocketClient(liveConsoleFeedUrl, delay, retryConnection);
}
private async Task ConnectWebSocketClient(string feedStreamUrl, TimeSpan delay, bool retryConnection = false)
{
bool connected = false;
int retries = 0;
do
{
try
{
Trace.Info($"Attempting to start websocket client with delay {delay}.");
await Task.Delay(delay);
using var connectTimeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), connectTimeoutTokenSource.Token);
Trace.Info($"Successfully started websocket client.");
connected = true;
}
catch (Exception ex)
{
Trace.Info("Exception caught during websocket client connect, retry connection.");
Trace.Error(ex);
retries++;
this._websocketClient = null;
_lastConnectionFailure = DateTime.Now;
}
} while (retryConnection && !connected && retries < 3);
}
public async Task<bool> AppendLiveConsoleFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, long? startLine, CancellationToken cancellationToken)
{
if (_websocketConnectTask != null)
{
await _websocketConnectTask;
}
bool delivered = false;
int retries = 0;
// "_websocketClient != null" implies either: We have a successful connection OR we have to attempt sending again and then reconnect
// ...in other words, if websocket client is null, we will skip sending to websocket
if (_websocketClient != null)
{
var linesWrapper = startLine.HasValue
? new TimelineRecordFeedLinesWrapper(stepId, lines, startLine.Value)
: new TimelineRecordFeedLinesWrapper(stepId, lines);
var jsonData = StringUtil.ConvertToJson(linesWrapper);
var jsonDataBytes = Encoding.UTF8.GetBytes(jsonData);
// break the message into chunks of 1024 bytes
for (var i = 0; i < jsonDataBytes.Length; i += 1 * 1024)
{
var lastChunk = i + (1 * 1024) >= jsonDataBytes.Length;
var chunk = new ArraySegment<byte>(jsonDataBytes, i, Math.Min(1 * 1024, jsonDataBytes.Length - i));
delivered = false;
while (!delivered && retries < 3)
{
try
{
if (_websocketClient != null)
{
await _websocketClient.SendAsync(chunk, WebSocketMessageType.Text, endOfMessage: lastChunk, cancellationToken);
delivered = true;
}
}
catch (Exception ex)
{
var delay = BackoffTimerHelper.GetRandomBackoff(MinDelayForWebsocketReconnect, MaxDelayForWebsocketReconnect);
Trace.Info($"Websocket is not open, let's attempt to connect back again with random backoff {delay} ms.");
Trace.Verbose(ex.ToString());
retries++;
InitializeWebsocketClient(_liveConsoleFeedUrl, _token, delay);
}
}
}
}
if (!delivered)
{
// Giving up for now, so next invocation of this method won't attempt to reconnect
_websocketClient = null;
// however if 10 minutes have already passed, let's try reestablish connection again
if (_lastConnectionFailure.HasValue && DateTime.Now > _lastConnectionFailure.Value.AddMinutes(10))
{
// Some minutes passed since we retried last time, try connection again
InitializeWebsocketClient(_liveConsoleFeedUrl, _token, TimeSpan.Zero);
}
}
return delivered;
}
private void CloseWebSocket(WebSocketCloseStatus closeStatus, CancellationToken cancellationToken)
{
try
{
_websocketClient?.CloseOutputAsync(closeStatus, "Closing websocket", cancellationToken);
}
catch (Exception websocketEx)
{
// In some cases this might be okay since the websocket might be open yet, so just close and don't trace exceptions
Trace.Info($"Failed to close websocket gracefully {websocketEx.GetType().Name}");
}
}
}
}

View File

@@ -19,14 +19,7 @@ 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,
IList<Annotation> jobAnnotations,
CancellationToken token);
Task CompleteJobAsync(Guid planId, Guid jobId, TaskResult result, Dictionary<String, VariableValue> outputs, IList<StepResult> stepResults, CancellationToken token);
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
}
@@ -58,30 +51,34 @@ namespace GitHub.Runner.Common
public Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken cancellationToken)
{
CheckConnection();
return RetryRequest<AgentJobRequestMessage>(
async () => await _runServiceHttpClient.GetJobMessageAsync(requestUri, id, cancellationToken), cancellationToken,
shouldRetry: ex => ex is not TaskOrchestrationJobAlreadyAcquiredException);
var jobMessage = RetryRequest<AgentJobRequestMessage>(
async () => await _runServiceHttpClient.GetJobMessageAsync(requestUri, id, cancellationToken), cancellationToken);
if (jobMessage == null)
{
throw new TaskOrchestrationJobNotFoundException(id);
}
return jobMessage;
}
public Task CompleteJobAsync(
Guid planId,
Guid jobId,
TaskResult result,
Dictionary<String, VariableValue> outputs,
IList<StepResult> stepResults,
IList<Annotation> jobAnnotations,
CancellationToken cancellationToken)
public Task CompleteJobAsync(Guid planId, Guid jobId, TaskResult result, Dictionary<String, VariableValue> outputs, IList<StepResult> stepResults, CancellationToken cancellationToken)
{
CheckConnection();
return RetryRequest(
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, cancellationToken), cancellationToken);
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, cancellationToken), cancellationToken);
}
public Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken cancellationToken)
{
CheckConnection();
return RetryRequest<RenewJobResponse>(
var renewJobResponse = RetryRequest<RenewJobResponse>(
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken);
if (renewJobResponse == null)
{
throw new TaskOrchestrationJobNotFoundException(jobId.ToString());
}
return renewJobResponse;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Sdk;
@@ -80,11 +80,10 @@ namespace GitHub.Runner.Common
}
await RetryRequest<Unit>(wrappedFunc, cancellationToken, maxRetryAttemptsCount);
}
protected async Task<T> RetryRequest<T>(Func<Task<T>> func,
CancellationToken cancellationToken,
int maxRetryAttemptsCount = 5,
Func<Exception, bool> shouldRetry = null
int maxRetryAttemptsCount = 5
)
{
var retryCount = 0;
@@ -97,7 +96,7 @@ namespace GitHub.Runner.Common
return await func();
}
// TODO: Add handling of non-retriable exceptions: https://github.com/github/actions-broker/issues/122
catch (Exception ex) when (retryCount < maxRetryAttemptsCount && (shouldRetry == null || shouldRetry(ex)))
catch (Exception ex) when (retryCount < maxRetryAttemptsCount)
{
Trace.Error("Catch exception during request");
Trace.Error(ex);

View File

@@ -29,8 +29,8 @@ namespace GitHub.Runner.Listener
private readonly Dictionary<string, string[]> validOptions = new()
{
// Valid configure flags and args
[Constants.Runner.CommandLine.Commands.Configure] =
new string[]
[Constants.Runner.CommandLine.Commands.Configure] =
new string[]
{
Constants.Runner.CommandLine.Flags.DisableUpdate,
Constants.Runner.CommandLine.Flags.Ephemeral,
@@ -38,7 +38,6 @@ namespace GitHub.Runner.Listener
Constants.Runner.CommandLine.Flags.Replace,
Constants.Runner.CommandLine.Flags.RunAsService,
Constants.Runner.CommandLine.Flags.Unattended,
Constants.Runner.CommandLine.Flags.NoDefaultLabels,
Constants.Runner.CommandLine.Args.Auth,
Constants.Runner.CommandLine.Args.Labels,
Constants.Runner.CommandLine.Args.MonitorSocketAddress,
@@ -86,7 +85,6 @@ namespace GitHub.Runner.Listener
public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral);
public bool GenerateServiceConfig => TestFlag(Constants.Runner.CommandLine.Flags.GenerateServiceConfig);
public bool Help => TestFlag(Constants.Runner.CommandLine.Flags.Help);
public bool NoDefaultLabels => TestFlag(Constants.Runner.CommandLine.Flags.NoDefaultLabels);
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
public bool RemoveLocalConfig => TestFlag(Constants.Runner.CommandLine.Flags.Local);
@@ -184,7 +182,7 @@ namespace GitHub.Runner.Listener
{
command = Constants.Runner.CommandLine.Commands.Warmup;
}
return command;
}

View File

@@ -137,7 +137,7 @@ namespace GitHub.Runner.Listener.Configuration
GitHubAuthResult authResult = await GetTenantCredential(inputUrl, registerToken, Constants.RunnerEvent.Register);
runnerSettings.ServerUrl = authResult.TenantUrl;
runnerSettings.UseV2Flow = authResult.UseV2Flow;
Trace.Info($"Using V2 flow: {runnerSettings.UseV2Flow}");
_term.WriteLine($"Using V2 flow: {runnerSettings.UseV2Flow}");
creds = authResult.ToVssCredentials();
Trace.Info("cred retrieved via GitHub auth");
}
@@ -259,7 +259,7 @@ namespace GitHub.Runner.Listener.Configuration
if (command.GetReplace())
{
// Update existing agent with new PublicKey, agent version.
agent = UpdateExistingAgent(agent, publicKey, userLabels, runnerSettings.Ephemeral, command.DisableUpdate, command.NoDefaultLabels);
agent = UpdateExistingAgent(agent, publicKey, userLabels, runnerSettings.Ephemeral, command.DisableUpdate);
try
{
@@ -293,7 +293,7 @@ namespace GitHub.Runner.Listener.Configuration
else
{
// Create a new agent.
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels, runnerSettings.Ephemeral, command.DisableUpdate, command.NoDefaultLabels);
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels, runnerSettings.Ephemeral, command.DisableUpdate);
try
{
@@ -554,7 +554,7 @@ namespace GitHub.Runner.Listener.Configuration
}
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral, bool disableUpdate, bool noDefaultLabels)
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral, bool disableUpdate)
{
ArgUtil.NotNull(agent, nameof(agent));
agent.Authorization = new TaskAgentAuthorization
@@ -571,16 +571,9 @@ namespace GitHub.Runner.Listener.Configuration
agent.Labels.Clear();
if (!noDefaultLabels)
{
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
}
else if (userLabels.Count == 0)
{
throw new NotSupportedException("Disabling default labels via --no-default-labels without specifying --labels is not supported");
}
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
foreach (var userLabel in userLabels)
{
@@ -590,7 +583,7 @@ namespace GitHub.Runner.Listener.Configuration
return agent;
}
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral, bool disableUpdate, bool noDefaultLabels)
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral, bool disableUpdate)
{
TaskAgent agent = new(agentName)
{
@@ -605,16 +598,9 @@ namespace GitHub.Runner.Listener.Configuration
DisableUpdate = disableUpdate
};
if (!noDefaultLabels)
{
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
}
else if (userLabels.Count == 0)
{
throw new NotSupportedException("Disabling default labels via --no-default-labels without specifying --labels is not supported");
}
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OS, LabelType.System));
agent.Labels.Add(new AgentLabel(VarUtil.OSArchitecture, LabelType.System));
foreach (var userLabel in userLabels)
{

View File

@@ -15,7 +15,6 @@ 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
@@ -373,8 +372,6 @@ 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())
@@ -382,6 +379,8 @@ 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);
@@ -406,7 +405,7 @@ namespace GitHub.Runner.Listener
await renewJobRequest;
// complete job request with result Cancelled
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
await CompleteJobRequestAsync(_poolId, message, lockToken, TaskResult.Canceled);
return;
}
@@ -545,6 +544,7 @@ 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, detailInfo);
await ForceFailJob(jobServer, message);
}
}
@@ -567,7 +567,7 @@ namespace GitHub.Runner.Listener
await renewJobRequest;
// complete job request
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, result, detailInfo);
await CompleteJobRequestAsync(_poolId, message, 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, systemConnection, lockToken, resultOnAbandonOrCancel);
await CompleteJobRequestAsync(_poolId, message, lockToken, resultOnAbandonOrCancel);
}
finally
{
@@ -1065,7 +1065,7 @@ namespace GitHub.Runner.Listener
}
}
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, ServiceEndpoint systemConnection, Guid lockToken, TaskResult result, string detailInfo = null)
private async Task CompleteJobRequestAsync(int poolId, Pipelines.AgentJobRequestMessage message, Guid lockToken, TaskResult result, string detailInfo = null)
{
Trace.Entering();
@@ -1077,23 +1077,7 @@ namespace GitHub.Runner.Listener
if (this._isRunServiceJob)
{
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);
}
Trace.Verbose($"Skip FinishAgentRequest call from Listener because MessageType is {message.MessageType}");
return;
}
@@ -1133,7 +1117,7 @@ namespace GitHub.Runner.Listener
}
// log an error issue to job level timeline record
private async Task LogWorkerProcessUnhandledException(IRunnerService server, Pipelines.AgentJobRequestMessage message, string detailInfo)
private async Task LogWorkerProcessUnhandledException(IRunnerService server, Pipelines.AgentJobRequestMessage message, string errorMessage)
{
if (server is IJobServer jobServer)
{
@@ -1145,11 +1129,34 @@ namespace GitHub.Runner.Listener
TimelineRecord jobRecord = timeline.Records.FirstOrDefault(x => x.Id == message.JobId && x.RecordType == "Job");
ArgUtil.NotNull(jobRecord, nameof(jobRecord));
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
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 };
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)
@@ -1160,13 +1167,13 @@ namespace GitHub.Runner.Listener
}
else
{
Trace.Info("Job server does not support handling unhandled exception yet, error message: {0}", detailInfo);
Trace.Info("Job server does not support handling unhandled exception yet, error message: {0}", errorMessage);
return;
}
}
// raise job completed event to fail the job.
private async Task ForceFailJob(IRunnerService server, Pipelines.AgentJobRequestMessage message, string detailInfo)
private async Task ForceFailJob(IRunnerService server, Pipelines.AgentJobRequestMessage message)
{
if (server is IJobServer jobServer)
{
@@ -1185,15 +1192,7 @@ namespace GitHub.Runner.Listener
{
try
{
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);
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, CancellationToken.None);
}
catch (Exception ex)
{

View File

@@ -549,17 +549,7 @@ namespace GitHub.Runner.Listener
{
var runServer = HostContext.CreateService<IRunServer>();
await runServer.ConnectAsync(new Uri(messageRef.RunServiceUrl), creds);
try
{
jobRequestMessage =
await runServer.GetJobMessageAsync(messageRef.RunnerRequestId,
messageQueueLoopTokenSource.Token);
}
catch (TaskOrchestrationJobAlreadyAcquiredException)
{
Trace.Info("Job is already acquired, skip this message.");
continue;
}
jobRequestMessage = await runServer.GetJobMessageAsync(messageRef.RunnerRequestId, messageQueueLoopTokenSource.Token);
}
jobDispatcher.Run(jobRequestMessage, runOnce);
@@ -683,8 +673,7 @@ Config Options:
--token string Registration token. Required if unattended
--name string Name of the runner to configure (default {Environment.MachineName ?? "myrunner"})
--runnergroup string Name of the runner group to add this runner to (defaults to the default runner group)
--labels string Custom labels that will be added to the runner. This option is mandatory if --no-default-labels is used.
--no-default-labels Disables adding the default labels: 'self-hosted,{Constants.Runner.Platform},{Constants.Runner.PlatformArchitecture}'
--labels string Extra labels in addition to the default: 'self-hosted,{Constants.Runner.Platform},{Constants.Runner.PlatformArchitecture}'
--local Removes the runner config files from your local machine. Used as an option to the remove command
--work string Relative runner work directory (default {Constants.Path.WorkDirectory})
--replace Replace any existing runner with the same name (default false)

View File

@@ -1,4 +1,4 @@
using System;
using System;
namespace GitHub.Runner.Sdk
{
@@ -11,11 +11,10 @@ namespace GitHub.Runner.Sdk
return false;
}
return
return
string.Equals(gitHubUrl.Host, "github.com", StringComparison.OrdinalIgnoreCase) ||
string.Equals(gitHubUrl.Host, "www.github.com", StringComparison.OrdinalIgnoreCase) ||
string.Equals(gitHubUrl.Host, "github.localhost", StringComparison.OrdinalIgnoreCase) ||
gitHubUrl.Host.EndsWith(".ghe.localhost", StringComparison.OrdinalIgnoreCase) ||
gitHubUrl.Host.EndsWith(".ghe.com", StringComparison.OrdinalIgnoreCase);
}

View File

@@ -60,7 +60,7 @@ namespace GitHub.Runner.Sdk
trace?.Verbose(ex.ToString());
}
if (matches != null && matches.Length > 0 && IsPathValid(matches.First(), trace))
if (matches != null && matches.Length > 0)
{
trace?.Info($"Location: '{matches.First()}'");
return matches.First();
@@ -86,7 +86,7 @@ namespace GitHub.Runner.Sdk
for (int i = 0; i < pathExtSegments.Length; i++)
{
string fullPath = Path.Combine(pathSegment, $"{command}{pathExtSegments[i]}");
if (matches.Any(p => p.Equals(fullPath, StringComparison.OrdinalIgnoreCase)) && IsPathValid(fullPath, trace))
if (matches.Any(p => p.Equals(fullPath, StringComparison.OrdinalIgnoreCase)))
{
trace?.Info($"Location: '{fullPath}'");
return fullPath;
@@ -105,7 +105,7 @@ namespace GitHub.Runner.Sdk
trace?.Verbose(ex.ToString());
}
if (matches != null && matches.Length > 0 && IsPathValid(matches.First(), trace))
if (matches != null && matches.Length > 0)
{
trace?.Info($"Location: '{matches.First()}'");
return matches.First();
@@ -128,15 +128,5 @@ namespace GitHub.Runner.Sdk
return null;
}
// checks if the file is a symlink and if the symlink`s target exists.
private static bool IsPathValid(string path, ITraceWriter trace = null)
{
var fileInfo = new FileInfo(path);
var linkTargetFullPath = fileInfo.Directory?.FullName + Path.DirectorySeparatorChar + fileInfo.LinkTarget;
if(fileInfo.LinkTarget == null || File.Exists(linkTargetFullPath) || File.Exists(fileInfo.LinkTarget)) return true;
trace?.Info($"the target '{fileInfo.LinkTarget}' of the symbolic link '{path}', does not exist");
return false;
}
}
}

View File

@@ -276,7 +276,7 @@ namespace GitHub.Runner.Worker
Message = $"Can't update {blocked} environment variable using ::set-env:: command."
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = $"{Constants.Runner.UnsupportedCommand}_{envName}";
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(issue);
return;
}
@@ -315,7 +315,7 @@ namespace GitHub.Runner.Worker
Message = String.Format(Constants.Runner.UnsupportedCommandMessage, this.Command)
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.UnsupportedCommand;
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(issue);
}
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
@@ -350,7 +350,7 @@ namespace GitHub.Runner.Worker
Message = String.Format(Constants.Runner.UnsupportedCommandMessage, this.Command)
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.UnsupportedCommand;
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(issue);
}
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
@@ -405,8 +405,9 @@ namespace GitHub.Runner.Worker
Trace.Info($"Add new secret mask with length of {command.Data.Length}");
// Also add each individual line. Typically individual lines are processed from STDOUT of child processes.
var split = command.Data.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var item in split)
var auxiliarySecrets = command.Data.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(candidate => candidate.Length >= HostContext.SecretMasker.DerivedSecretRecommendedMinimumLength);
foreach (var item in auxiliarySecrets)
{
HostContext.SecretMasker.AddValue(item);
}
@@ -666,7 +667,7 @@ namespace GitHub.Runner.Worker
}
}
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(issue);
}
public static void ValidateLinesAndColumns(ActionCommand command, IExecutionContext context)

View File

@@ -11,14 +11,12 @@ using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
using GitHub.Services.Common;
using WebApi = GitHub.DistributedTask.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants;
using GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Worker
{
@@ -102,19 +100,7 @@ namespace GitHub.Runner.Worker
}
IEnumerable<Pipelines.ActionStep> actions = steps.OfType<Pipelines.ActionStep>();
executionContext.Output("Prepare all required actions");
PrepareActionsState result = new PrepareActionsState();
try
{
result = await PrepareActionsRecursiveAsync(executionContext, state, actions, depth, rootStepId);
}
catch (FailedToResolveActionDownloadInfoException ex)
{
// Log the error and fail the PrepareActionsAsync Initialization.
Trace.Error($"Caught exception from PrepareActionsAsync Initialization: {ex}");
executionContext.InfrastructureError(ex.Message);
executionContext.Result = TaskResult.Failed;
throw;
}
var result = await PrepareActionsRecursiveAsync(executionContext, state, actions, depth, rootStepId);
if (!FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
{
if (state.ImagesToPull.Count > 0)
@@ -317,28 +303,15 @@ namespace GitHub.Runner.Worker
if (action.Reference.Type == Pipelines.ActionSourceType.ContainerRegistry)
{
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
Trace.Info("Load action that reference container from registry.");
CachedActionContainers.TryGetValue(action.Id, out var container);
ArgUtil.NotNull(container, nameof(container));
definition.Data.Execution = new ContainerActionExecutionData()
{
Trace.Info("Load action that will run container through container hooks.");
var containerAction = action.Reference as Pipelines.ContainerRegistryReference;
definition.Data.Execution = new ContainerActionExecutionData()
{
Image = containerAction.Image,
};
Trace.Info($"Using action container image: {containerAction.Image}.");
}
else
{
Trace.Info("Load action that reference container from registry.");
CachedActionContainers.TryGetValue(action.Id, out var container);
ArgUtil.NotNull(container, nameof(container));
definition.Data.Execution = new ContainerActionExecutionData()
{
Image = container.ContainerImage
};
Image = container.ContainerImage
};
Trace.Info($"Using action container image: {container.ContainerImage}.");
}
Trace.Info($"Using action container image: {container.ContainerImage}.");
}
else if (action.Reference.Type == Pipelines.ActionSourceType.Repository)
{
@@ -675,21 +648,13 @@ namespace GitHub.Runner.Worker
}
// Resolve download info
var launchServer = HostContext.GetService<ILaunchServer>();
var jobServer = HostContext.GetService<IJobServer>();
var actionDownloadInfos = default(WebApi.ActionDownloadInfoCollection);
for (var attempt = 1; attempt <= 3; attempt++)
{
try
{
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
{
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
}
else
{
actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
}
actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Global.Plan.ScopeIdentifier, executionContext.Global.Plan.PlanType, executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken);
break;
}
catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is cancelled.

View File

@@ -52,7 +52,7 @@ namespace GitHub.Runner.Worker
public ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile)
{
var templateContext = CreateTemplateContext(executionContext, null, LogTemplateErrorsToTraceWriter(executionContext));
var templateContext = CreateTemplateContext(executionContext);
ActionDefinitionData actionDefinition = new();
// Clean up file name real quick
@@ -303,8 +303,7 @@ namespace GitHub.Runner.Worker
private TemplateContext CreateTemplateContext(
IExecutionContext executionContext,
IDictionary<string, PipelineContextData> extraExpressionValues = null,
bool addErrorsToTraceWriter = true)
IDictionary<string, PipelineContextData> extraExpressionValues = null)
{
var result = new TemplateContext
{
@@ -316,7 +315,6 @@ namespace GitHub.Runner.Worker
maxBytes: 10 * 1024 * 1024),
Schema = _actionManifestSchema,
TraceWriter = executionContext.ToTemplateTraceWriter(),
LogErrorsToTraceWriter = addErrorsToTraceWriter
};
// Expression values from execution context
@@ -450,7 +448,7 @@ namespace GitHub.Runner.Worker
};
}
}
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase)||
string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrEmpty(mainToken?.Value))
@@ -541,13 +539,6 @@ namespace GitHub.Runner.Worker
}
}
}
private bool LogTemplateErrorsToTraceWriter(IExecutionContext executionContext)
{
if (executionContext == null || executionContext.Global == null || executionContext.Global.EnvironmentVariables == null) return true;
executionContext.Global.EnvironmentVariables.TryGetValue(Constants.Runner.Features.LogTemplateErrorsToTraceWriter, out var logErrorsAsDebug);
return StringUtil.ConvertToBoolean(logErrorsAsDebug, defaultValue: true);
}
}
}

View File

@@ -83,7 +83,7 @@ namespace GitHub.Runner.Worker.Container.ContainerHooks
public HookContainer(ContainerInfo container)
{
Image = container.ContainerImage;
EntryPointArgs = container.ContainerEntryPointArgs?.Split(' ').Select(arg => arg.Trim()).Where(arg => !string.IsNullOrEmpty(arg)) ?? new List<string>();
EntryPointArgs = container.ContainerEntryPointArgs?.Split(' ').Select(arg => arg.Trim()) ?? new List<string>();
EntryPoint = container.ContainerEntryPoint;
WorkingDirectory = container.ContainerWorkDirectory;
CreateOptions = container.ContainerCreateOptions;

View File

@@ -18,23 +18,15 @@ 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;
using constants = GitHub.Runner.Common.Constants;
namespace GitHub.Runner.Worker
{
public static class ExecutionContextType
public class ExecutionContextType
{
public const string Job = "Job";
public const string Task = "Task";
}
public record ExecutionContextLogOptions(bool WriteToLog, string LogMessageOverride)
{
public static readonly ExecutionContextLogOptions None = new(false, null);
public static readonly ExecutionContextLogOptions Default = new(true, null);
public static string Job = "Job";
public static string Task = "Task";
}
[ServiceLocator(Default = typeof(ExecutionContext))]
@@ -100,7 +92,7 @@ namespace GitHub.Runner.Worker
void SetGitHubContext(string name, string value);
void SetOutput(string name, string value, out string reference);
void SetTimeout(TimeSpan? timeout);
void AddIssue(Issue issue, ExecutionContextLogOptions logOptions);
void AddIssue(Issue issue, string message = null);
void Progress(int percentage, string currentOperation = null);
void UpdateDetailTimelineRecord(TimelineRecord record);
@@ -126,7 +118,7 @@ namespace GitHub.Runner.Worker
public sealed class ExecutionContext : RunnerService, IExecutionContext
{
private const int _maxCountPerIssueType = 10;
private const int _maxIssueCount = 10;
private const int _throttlingDelayReportThreshold = 10 * 1000; // Don't report throttling with less than 10 seconds delay
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
@@ -134,10 +126,8 @@ namespace GitHub.Runner.Worker
private readonly TimelineRecord _record = new();
private readonly Dictionary<Guid, TimelineRecord> _detailRecords = new();
private readonly List<Issue> _embeddedIssueCollector;
private readonly object _loggerLock = new();
private readonly object _matchersLock = new();
private readonly ExecutionContext _parentExecutionContext;
private event OnMatcherChanged _onMatcherChanged;
@@ -145,6 +135,7 @@ namespace GitHub.Runner.Worker
private IPagingLogger _logger;
private IJobServerQueue _jobServerQueue;
private ExecutionContext _parentExecutionContext;
private Guid _mainTimelineId;
private Guid _detailTimelineId;
@@ -158,29 +149,6 @@ namespace GitHub.Runner.Worker
private long _totalThrottlingDelayInMilliseconds = 0;
private bool _stepTelemetryPublished = false;
public ExecutionContext()
: this(parent: null, embedded: false)
{
}
private ExecutionContext(ExecutionContext parent, bool embedded)
{
if (embedded)
{
ArgUtil.NotNull(parent, nameof(parent));
}
_parentExecutionContext = parent;
this.IsEmbedded = embedded;
this.StepTelemetry = new ActionsStepTelemetry
{
IsEmbedded = embedded
};
//Embedded Execution Contexts pseudo-inherit their parent's embeddedIssueCollector.
_embeddedIssueCollector = embedded ? parent._embeddedIssueCollector : new();
}
public Guid Id => _record.Id;
public Guid EmbeddedId { get; private set; }
public string ScopeName { get; private set; }
@@ -193,7 +161,7 @@ namespace GitHub.Runner.Worker
public Dictionary<string, VariableValue> JobOutputs { get; private set; }
public ActionsEnvironmentReference ActionsEnvironment { get; private set; }
public ActionsStepTelemetry StepTelemetry { get; private init; }
public ActionsStepTelemetry StepTelemetry { get; } = new ActionsStepTelemetry();
public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData();
public IList<IFunctionInfo> ExpressionFunctions { get; } = new List<IFunctionInfo>();
@@ -218,7 +186,7 @@ namespace GitHub.Runner.Worker
// An embedded execution context shares the same record ID, record name, and logger
// as its enclosing execution context.
public bool IsEmbedded { get; private init; }
public bool IsEmbedded { get; private set; }
public TaskResult? Result
{
@@ -353,7 +321,7 @@ namespace GitHub.Runner.Worker
{
Trace.Entering();
var child = new ExecutionContext(this, isEmbedded);
var child = new ExecutionContext();
child.Initialize(HostContext);
child.Global = Global;
child.ScopeName = scopeName;
@@ -378,6 +346,7 @@ namespace GitHub.Runner.Worker
child.ExpressionFunctions.Add(item);
}
child._cancellationTokenSource = cancellationTokenSource ?? new CancellationTokenSource();
child._parentExecutionContext = this;
child.EchoOnActionCommand = EchoOnActionCommand;
if (recordOrder != null)
@@ -398,9 +367,11 @@ namespace GitHub.Runner.Worker
child._logger.Setup(_mainTimelineId, recordId);
}
child.IsEmbedded = isEmbedded;
child.StepTelemetry.StepId = recordId;
child.StepTelemetry.Stage = stage.ToString();
child.StepTelemetry.StepContextName = child.GetFullyQualifiedContextName();
child.StepTelemetry.IsEmbedded = isEmbedded;
child.StepTelemetry.StepContextName = child.GetFullyQualifiedContextName(); ;
return child;
}
@@ -442,24 +413,13 @@ namespace GitHub.Runner.Worker
this.Warning($"The job has experienced {TimeSpan.FromMilliseconds(_totalThrottlingDelayInMilliseconds).TotalSeconds} seconds total delay caused by server throttling.");
}
DateTime now = DateTime.UtcNow;
_record.CurrentOperation = currentOperation ?? _record.CurrentOperation;
_record.ResultCode = resultCode ?? _record.ResultCode;
_record.FinishTime = now;
_record.FinishTime = DateTime.UtcNow;
_record.PercentComplete = 100;
_record.Result = _record.Result ?? TaskResult.Succeeded;
_record.State = TimelineRecordState.Completed;
// Before our main timeline's final QueueTimelineRecordUpdate,
// inject any issues collected by embedded ExecutionContexts.
if (!this.IsEmbedded)
{
foreach (var issue in _embeddedIssueCollector)
{
AddIssue(issue, ExecutionContextLogOptions.None);
}
}
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
// complete all detail timeline records.
@@ -467,7 +427,7 @@ namespace GitHub.Runner.Worker
{
foreach (var record in _detailRecords)
{
record.Value.FinishTime = record.Value.FinishTime ?? now;
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;
@@ -478,26 +438,14 @@ namespace GitHub.Runner.Worker
PublishStepTelemetry();
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);
}
});
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;
Global.StepsResult.Add(stepResult);
@@ -611,10 +559,14 @@ namespace GitHub.Runner.Worker
}
// This is not thread safe, the caller need to take lock before calling issue()
public void AddIssue(Issue issue, ExecutionContextLogOptions logOptions)
public void AddIssue(Issue issue, string logMessage = null)
{
ArgUtil.NotNull(issue, nameof(issue));
ArgUtil.NotNull(logOptions, nameof(logOptions));
if (string.IsNullOrEmpty(logMessage))
{
logMessage = issue.Message;
}
issue.Message = HostContext.SecretMasker.MaskSecrets(issue.Message);
if (issue.Message.Length > _maxIssueMessageLength)
@@ -629,64 +581,53 @@ namespace GitHub.Runner.Worker
issue.Data["stepNumber"] = _record.Order.ToString();
}
string wellKnownTag = null;
Int32 previousCountForIssueType = 0;
Action incrementIssueTypeCount = NoOp;
switch (issue.Type)
if (issue.Type == IssueType.Error)
{
case IssueType.Error:
wellKnownTag = WellKnownTags.Error;
previousCountForIssueType = _record.ErrorCount;
incrementIssueTypeCount = () => { _record.ErrorCount++; };
break;
case IssueType.Warning:
wellKnownTag = WellKnownTags.Warning;
previousCountForIssueType = _record.WarningCount;
incrementIssueTypeCount = () => { _record.WarningCount++; };
break;
case IssueType.Notice:
wellKnownTag = WellKnownTags.Notice;
previousCountForIssueType = _record.NoticeCount;
incrementIssueTypeCount = () => { _record.NoticeCount++; };
break;
}
if (!string.IsNullOrEmpty(wellKnownTag))
{
if (!this.IsEmbedded && previousCountForIssueType < _maxCountPerIssueType)
if (!string.IsNullOrEmpty(logMessage))
{
long logLineNumber = Write(WellKnownTags.Error, logMessage);
issue.Data["logFileLineNumber"] = logLineNumber.ToString();
}
if (_record.ErrorCount < _maxIssueCount)
{
incrementIssueTypeCount();
_record.Issues.Add(issue);
}
if (logOptions.WriteToLog)
_record.ErrorCount++;
}
else if (issue.Type == IssueType.Warning)
{
if (!string.IsNullOrEmpty(logMessage))
{
string logMessage = issue.Message;
if (!string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
logMessage = logOptions.LogMessageOverride;
}
if (!string.IsNullOrEmpty(logMessage))
{
// Note that ::Write() has its own secret-masking logic.
long logLineNumber = Write(wellKnownTag, logMessage);
issue.Data["logFileLineNumber"] = logLineNumber.ToString();
}
long logLineNumber = Write(WellKnownTags.Warning, logMessage);
issue.Data["logFileLineNumber"] = logLineNumber.ToString();
}
if (_record.WarningCount < _maxIssueCount)
{
_record.Issues.Add(issue);
}
_record.WarningCount++;
}
else if (issue.Type == IssueType.Notice)
{
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++;
}
// Embedded ExecutionContexts (a.k.a. Composite actions) should never upload a timeline record to the server.
// Instead, we store processed issues on a shared (psuedo-inherited) list (belonging to the closest
// non-embedded ancestor ExecutionContext) so that they can be processed when that ancestor completes.
if (this.IsEmbedded)
{
_embeddedIssueCollector.Add(issue);
}
else
{
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
}
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
}
public void UpdateDetailTimelineRecord(TimelineRecord record)
@@ -784,9 +725,6 @@ 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);
@@ -803,9 +741,6 @@ namespace GitHub.Runner.Worker
// File table
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);
// What type of job request is running (i.e. Run Service vs. pipelines)
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);
// Expression values
if (message.ContextData?.Count > 0)
{
@@ -1060,7 +995,8 @@ namespace GitHub.Runner.Worker
StepTelemetry.FinishTime = _record.FinishTime;
}
if (!IsEmbedded)
if (!IsEmbedded &&
_record.Issues.Count > 0)
{
foreach (var issue in _record.Issues)
{
@@ -1226,11 +1162,6 @@ namespace GitHub.Runner.Worker
UpdateGlobalStepsContext();
}
private static void NoOp()
{
}
}
// The Error/Warning/etc methods are created as extension methods to simplify unit testing.
@@ -1255,22 +1186,19 @@ namespace GitHub.Runner.Worker
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Error(this IExecutionContext context, string message)
{
var issue = new Issue() { Type = IssueType.Error, Message = message };
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(new Issue() { Type = IssueType.Error, Message = message });
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void InfrastructureError(this IExecutionContext context, string message)
{
var issue = new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true };
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true });
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Warning(this IExecutionContext context, string message)
{
var issue = new Issue() { Type = IssueType.Warning, Message = message };
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(new Issue() { Type = IssueType.Warning, Message = message });
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
@@ -1426,5 +1354,4 @@ namespace GitHub.Runner.Worker
public static readonly string Notice = "##[notice]";
public static readonly string Debug = "##[debug]";
}
}

View File

@@ -1,11 +1,10 @@
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
{
@@ -19,7 +18,6 @@ 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; }

View File

@@ -294,7 +294,7 @@ namespace GitHub.Runner.Worker.Handlers
// Evaluation error
Trace.Info("Caught exception from expression for embedded step.env");
step.ExecutionContext.Error(ex);
SetStepConclusion(step, TaskResult.Failed);
step.ExecutionContext.Complete(TaskResult.Failed);
}
// Register Callback

View File

@@ -143,8 +143,7 @@ namespace GitHub.Runner.Worker.Handlers
if (issue != null)
{
// Log issue
var logOptions = new ExecutionContextLogOptions(true, stripped);
_executionContext.AddIssue(issue, logOptions);
_executionContext.AddIssue(issue, stripped);
return;
}

View File

@@ -431,6 +431,14 @@ namespace GitHub.Runner.Worker
context.Result = TaskResult.Canceled;
throw;
}
catch (FailedToResolveActionDownloadInfoException ex)
{
// Log the error and fail the JobExtension Initialization.
Trace.Error($"Caught exception from JobExtenion Initialization: {ex}");
context.InfrastructureError(ex.Message);
context.Result = TaskResult.Failed;
throw;
}
catch (Exception ex)
{
// Log the error and fail the JobExtension Initialization.
@@ -675,7 +683,7 @@ namespace GitHub.Runner.Worker
{
var issue = new Issue() { Type = IssueType.Warning, Message = $"You are running out of disk space. The runner will stop working when the machine runs out of disk space. Free space left: {freeSpaceInMB} MB" };
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.LowDiskSpace;
context.AddIssue(issue, ExecutionContextLogOptions.Default);
context.AddIssue(issue);
return;
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -43,13 +42,6 @@ namespace GitHub.Runner.Worker
DateTime jobStartTimeUtc = DateTime.UtcNow;
IRunnerService server = null;
// add orchestration id to useragent for better correlation.
if (message.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out VariableValue orchestrationId) &&
!string.IsNullOrEmpty(orchestrationId.Value))
{
HostContext.UserAgents.Add(new ProductInfoHeaderValue("OrchestrationId", orchestrationId.Value));
}
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
if (MessageUtil.IsRunServiceJob(message.MessageType))
{
@@ -57,21 +49,6 @@ namespace GitHub.Runner.Worker
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
server = runServer;
message.Variables.TryGetValue("system.github.launch_endpoint", out VariableValue launchEndpointVariable);
var launchReceiverEndpoint = launchEndpointVariable?.Value;
if (systemConnection?.Authorization != null &&
systemConnection.Authorization.Parameters.TryGetValue("AccessToken", out var accessToken) &&
!string.IsNullOrEmpty(accessToken) &&
!string.IsNullOrEmpty(launchReceiverEndpoint))
{
Trace.Info("Initializing launch client");
var launchServer = HostContext.GetService<ILaunchServer>();
launchServer.InitializeLaunchClient(new Uri(launchReceiverEndpoint), accessToken);
}
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
_jobServerQueue.Start(message, resultServiceOnly: true);
}
else
{
@@ -120,8 +97,7 @@ namespace GitHub.Runner.Worker
default:
throw new ArgumentException(HostContext.RunnerShutdownReason.ToString(), nameof(HostContext.RunnerShutdownReason));
}
var issue = new Issue() { Type = IssueType.Error, Message = errorMessage };
jobContext.AddIssue(issue, ExecutionContextLogOptions.Default);
jobContext.AddIssue(new Issue() { Type = IssueType.Error, Message = errorMessage });
});
// Validate directory permissions.
@@ -150,11 +126,6 @@ namespace GitHub.Runner.Worker
_runnerSettings = HostContext.GetService<IConfigurationStore>().GetSettings();
jobContext.SetRunnerContext("name", _runnerSettings.AgentName);
if (jobContext.Global.Variables.TryGetValue(WellKnownDistributedTaskVariables.RunnerEnvironment, out var runnerEnvironment))
{
jobContext.SetRunnerContext("environment", runnerEnvironment);
}
string toolsDirectory = HostContext.GetDirectory(WellKnownDirectory.Tools);
Directory.CreateDirectory(toolsDirectory);
jobContext.SetRunnerContext("tool_cache", toolsDirectory);
@@ -290,7 +261,7 @@ namespace GitHub.Runner.Worker
{
try
{
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, default);
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, default);
return result;
}
catch (Exception ex)

View File

@@ -1,12 +1,12 @@
using GitHub.DistributedTask.WebApi;
using GitHub.DistributedTask.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Common.Util;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Services.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using System.Text;
@@ -156,8 +156,9 @@ namespace GitHub.Runner.Worker
HostContext.SecretMasker.AddValue(value);
// Also add each individual line. Typically individual lines are processed from STDOUT of child processes.
var split = value.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var item in split)
var auxiliarySecrets = value.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(candidate => candidate.Length >= HostContext.SecretMasker.DerivedSecretRecommendedMinimumLength);
foreach (var item in auxiliarySecrets)
{
HostContext.SecretMasker.AddValue(item);
}

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System;
using System.ComponentModel;
using System.Linq;
using GitHub.DistributedTask.Logging;
namespace GitHub.DistributedTask.Expressions2.Sdk
@@ -55,7 +55,7 @@ namespace GitHub.DistributedTask.Expressions2.Sdk
try
{
// Evaluate
secretMasker = secretMasker?.Clone() ?? new SecretMasker();
secretMasker = secretMasker?.Clone() ?? new SecretMasker(Enumerable.Empty<ValueEncoder>());
trace = new EvaluationTraceWriter(trace, secretMasker);
var context = new EvaluationContext(trace, secretMasker, state, options, this);
trace.Info($"Evaluating: {ConvertToExpression()}");

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel;
namespace GitHub.DistributedTask.Logging
@@ -6,9 +6,9 @@ namespace GitHub.DistributedTask.Logging
[EditorBrowsable(EditorBrowsableState.Never)]
public interface ISecretMasker
{
int DerivedSecretRecommendedMinimumLength { get; }
void AddRegex(String pattern);
void AddValue(String value);
void AddValueEncoder(ValueEncoder encoder);
ISecretMasker Clone();
String MaskSecrets(String input);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -10,11 +10,11 @@ namespace GitHub.DistributedTask.Logging
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class SecretMasker : ISecretMasker, IDisposable
{
public SecretMasker()
public SecretMasker(IEnumerable<ValueEncoder> encoders)
{
m_originalValueSecrets = new HashSet<ValueSecret>();
m_regexSecrets = new HashSet<RegexSecret>();
m_valueEncoders = new HashSet<ValueEncoder>();
m_valueEncoders = new HashSet<ValueEncoder>(encoders ?? Enumerable.Empty<ValueEncoder>());
m_valueSecrets = new HashSet<ValueSecret>();
}
@@ -40,6 +40,19 @@ namespace GitHub.DistributedTask.Logging
}
}
/// <summary>
/// Provide callers with a recommendation on what to consider a secret.
/// This is helpful in cases where JSON (for example) is broken into multiple lines
/// and we don't want to start treating standalone JSON control characters as secrets.
/// </summary>
public int DerivedSecretRecommendedMinimumLength
{
get
{
return 3;
}
}
/// <summary>
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
/// </summary>
@@ -104,15 +117,11 @@ namespace GitHub.DistributedTask.Logging
}
}
// Compute the encoded values.
foreach (ValueEncoder valueEncoder in valueEncoders)
{
String encodedValue = valueEncoder(value);
if (!String.IsNullOrEmpty(encodedValue))
{
valueSecrets.Add(new ValueSecret(encodedValue));
}
}
var secretVariations = valueEncoders.SelectMany(encoder => encoder(value))
.Where(variation => !string.IsNullOrEmpty(variation))
.Distinct()
.Select(variation => new ValueSecret(variation));
valueSecrets.AddRange(secretVariations);
// Write section.
try
@@ -135,69 +144,6 @@ namespace GitHub.DistributedTask.Logging
}
}
/// <summary>
/// This implementation assumes no more than one thread is adding regexes, values, or encoders at any given time.
/// </summary>
public void AddValueEncoder(ValueEncoder encoder)
{
ValueSecret[] originalSecrets;
// Read section.
try
{
m_lock.EnterReadLock();
// Test whether already added.
if (m_valueEncoders.Contains(encoder))
{
return;
}
// Read the original value secrets.
originalSecrets = m_originalValueSecrets.ToArray();
}
finally
{
if (m_lock.IsReadLockHeld)
{
m_lock.ExitReadLock();
}
}
// Compute the encoded values.
var encodedSecrets = new List<ValueSecret>();
foreach (ValueSecret originalSecret in originalSecrets)
{
String encodedValue = encoder(originalSecret.m_value);
if (!String.IsNullOrEmpty(encodedValue))
{
encodedSecrets.Add(new ValueSecret(encodedValue));
}
}
// Write section.
try
{
m_lock.EnterWriteLock();
// Add the encoder.
m_valueEncoders.Add(encoder);
// Add the values.
foreach (ValueSecret encodedSecret in encodedSecrets)
{
m_valueSecrets.Add(encodedSecret);
}
}
finally
{
if (m_lock.IsWriteLockHeld)
{
m_lock.ExitWriteLock();
}
}
}
public ISecretMasker Clone() => new SecretMasker(this);
public void Dispose()

View File

@@ -1,73 +1,97 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
namespace GitHub.DistributedTask.Logging
{
[EditorBrowsable(EditorBrowsableState.Never)]
public delegate String ValueEncoder(String value);
public delegate IEnumerable<string> ValueEncoder(string value);
[EditorBrowsable(EditorBrowsableState.Never)]
public static class ValueEncoders
{
public static String Base64StringEscape(String value)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
}
// Base64 is 6 bits -> char
// A byte is 8 bits
// When end user doing somthing like base64(user:password)
// The length of the leading content will cause different base64 encoding result on the password
// So we add base64(value shifted 1 and two bytes) as secret as well.
// B1 B2 B3 B4 B5 B6 B7
// 000000|00 0000|0000 00|000000| 000000|00 0000|0000 00|000000|
// Char1 Char2 Char3 Char4
// See the above, the first byte has a character beginning at index 0, the second byte has a character beginning at index 4, the third byte has a character beginning at index 2 and then the pattern repeats
// We register byte offsets for all these possible values
public static String Base64StringEscapeShift1(String value)
public static IEnumerable<string> EnumerateBase64Variations(string value)
{
return Base64StringEscapeShift(value, 1);
}
if (!string.IsNullOrEmpty(value))
{
// A byte is 8 bits. A Base64 "digit" can hold a maximum of 6 bits (2^64 - 1, or values 0 to 63).
// As a result, many Unicode characters (including single-byte letters) cannot be represented using a single Base64 digit.
// Furthermore, on average a Base64 string will be about 33% longer than the original text.
// This is because it generally requires 4 Base64 digits to represent 3 Unicode bytes. (4 / 3 ~ 1.33)
//
// Because of this 4:3 ratio (or, more precisely, 8 bits : 6 bits ratio), there's a cyclical pattern
// to when a byte boundary aligns with a Base64 digit boundary.
// The pattern repeats every 24 bits (the lowest common multiple of 8 and 6).
//
// |-----------24 bits-------------|-----------24 bits------------|
// Base64 Digits: |digit 0|digit 1|digit 2|digit 3|digit 4|digit 5|digit 6|digit7|
// Allocated Bits: aaaaaa aaBBBB BBBBcc cccccc DDDDDD DDeeee eeeeFF FFFFFF
// Unicode chars: |0th char |1st char |2nd char |3rd char |4th char |5th char |
public static String Base64StringEscapeShift2(String value)
{
return Base64StringEscapeShift(value, 2);
// Depending on alignment, the Base64-encoded secret can take any of 3 basic forms.
// For example, the Base64 digits representing "abc" could appear as any of the following:
// "YWJj" when aligned
// ".!FiYw==" when preceded by 3x + 1 bytes
// "..!hYmM=" when preceded by 3x + 2 bytes
// (where . represents an unrelated Base64 digit, ! represents a Base64 digit that should be masked, and x represents any non-negative integer)
var rawBytes = Encoding.UTF8.GetBytes(value);
for (var offset = 0; offset <= 2; offset++)
{
var prunedBytes = rawBytes.Skip(offset).ToArray();
if (prunedBytes.Length > 0)
{
// Don't include Base64 padding characters (=) in Base64 representations of the secret.
// They don't represent anything interesting, so they don't need to be masked.
// (Some clients omit the padding, so we want to be sure we recognize the secret regardless of whether the padding is present or not.)
var buffer = new StringBuilder(Convert.ToBase64String(prunedBytes).TrimEnd(BASE64_PADDING_SUFFIX));
yield return buffer.ToString();
// Also, yield the RFC4648-equivalent RegEx.
buffer.Replace('+', '-');
buffer.Replace('/', '_');
yield return buffer.ToString();
}
}
}
}
// Used when we pass environment variables to docker to escape " with \"
public static String CommandLineArgumentEscape(String value)
public static IEnumerable<string> CommandLineArgumentEscape(string value)
{
return value.Replace("\"", "\\\"");
yield return value.Replace("\"", "\\\"");
}
public static String ExpressionStringEscape(String value)
public static IEnumerable<string> ExpressionStringEscape(string value)
{
return Expressions2.Sdk.ExpressionUtility.StringEscape(value);
yield return Expressions2.Sdk.ExpressionUtility.StringEscape(value);
}
public static String JsonStringEscape(String value)
public static IEnumerable<string> JsonStringEscape(string value)
{
// Convert to a JSON string and then remove the leading/trailing double-quote.
String jsonString = JsonConvert.ToString(value);
String jsonEscapedValue = jsonString.Substring(startIndex: 1, length: jsonString.Length - 2);
return jsonEscapedValue;
yield return jsonEscapedValue;
}
public static String UriDataEscape(String value)
public static IEnumerable<string> UriDataEscape(string value)
{
return UriDataEscape(value, 65519);
yield return UriDataEscape(value, 65519);
}
public static String XmlDataEscape(String value)
public static IEnumerable<string> XmlDataEscape(string value)
{
return SecurityElement.Escape(value);
yield return SecurityElement.Escape(value);
}
public static String TrimDoubleQuotes(String value)
public static IEnumerable<string> TrimDoubleQuotes(string value)
{
var trimmed = string.Empty;
if (!string.IsNullOrEmpty(value) &&
@@ -78,17 +102,17 @@ namespace GitHub.DistributedTask.Logging
trimmed = value.Substring(1, value.Length - 2);
}
return trimmed;
yield return trimmed;
}
public static String PowerShellPreAmpersandEscape(String value)
public static IEnumerable<string> PowerShellPreAmpersandEscape(string value)
{
// if the secret is passed to PS as a command and it causes an error, sections of it can be surrounded by color codes
// or printed individually.
// or printed individually.
// The secret secretpart1&secretpart2&secretpart3 would be split into 2 sections:
// 'secretpart1&secretpart2&' and 'secretpart3'. This method masks for the first section.
// The secret secretpart1&+secretpart2&secretpart3 would be split into 2 sections:
// 'secretpart1&+' and (no 's') 'ecretpart2&secretpart3'. This method masks for the first section.
@@ -112,10 +136,10 @@ namespace GitHub.DistributedTask.Logging
}
}
return trimmed;
yield return trimmed;
}
public static String PowerShellPostAmpersandEscape(String value)
public static IEnumerable<string> PowerShellPostAmpersandEscape(string value)
{
var trimmed = string.Empty;
if (!string.IsNullOrEmpty(value) && value.Contains("&"))
@@ -137,27 +161,10 @@ namespace GitHub.DistributedTask.Logging
}
}
return trimmed;
yield return trimmed;
}
private static string Base64StringEscapeShift(String value, int shift)
{
var bytes = Encoding.UTF8.GetBytes(value);
if (bytes.Length > shift)
{
var shiftArray = new byte[bytes.Length - shift];
Array.Copy(bytes, shift, shiftArray, 0, bytes.Length - shift);
return Convert.ToBase64String(shiftArray);
}
else
{
return Convert.ToBase64String(bytes);
}
}
private static String UriDataEscape(
String value,
Int32 maxSegmentSize)
private static string UriDataEscape(string value, Int32 maxSegmentSize)
{
if (value.Length <= maxSegmentSize)
{
@@ -183,5 +190,7 @@ namespace GitHub.DistributedTask.Logging
return result.ToString();
}
private const char BASE64_PADDING_SUFFIX = '=';
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -34,11 +34,6 @@ namespace GitHub.DistributedTask.ObjectTemplating
m_errors = value;
}
}
/// <summary>
/// Because TraceWriter has access to the ExecutionContext and is logging template errors, duplicated issues are reported.
/// By setting LogErrorsToTraceWriter = false TemplateContext will add errors only to TemplateValidationErrors
/// </summary>
internal bool LogErrorsToTraceWriter = true;
/// <summary>
/// Available functions within expression contexts
@@ -125,9 +120,15 @@ namespace GitHub.DistributedTask.ObjectTemplating
}
}
internal void Error(TemplateValidationError error)
{
Errors.Add(error);
TraceWriter.Error(error.Message);
}
internal void Error(
TemplateToken value,
Exception ex)
TemplateToken value,
Exception ex)
{
Error(value?.FileId, value?.Line, value?.Column, ex);
}
@@ -140,11 +141,7 @@ namespace GitHub.DistributedTask.ObjectTemplating
{
var prefix = GetErrorPrefix(fileId, line, column);
Errors.Add(prefix, ex);
if (LogErrorsToTraceWriter)
{
TraceWriter.Error(prefix, ex);
}
TraceWriter.Error(prefix, ex);
}
internal void Error(
@@ -161,13 +158,13 @@ namespace GitHub.DistributedTask.ObjectTemplating
String message)
{
var prefix = GetErrorPrefix(fileId, line, column);
var fullMessage = !String.IsNullOrEmpty(prefix) ? $"{prefix} {message}" : message;
Errors.Add(fullMessage);
if (LogErrorsToTraceWriter)
if (!String.IsNullOrEmpty(prefix))
{
TraceWriter.Error(fullMessage);
message = $"{prefix} {message}";
}
Errors.Add(message);
TraceWriter.Error(message);
}
internal INamedValueInfo[] GetExpressionNamedValues()

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.Serialization;
using GitHub.Services.Common;
@@ -1519,26 +1519,6 @@ namespace GitHub.DistributedTask.WebApi
}
}
[Serializable]
[ExceptionMapping("0.0", "3.0", "TaskOrchestrationJobAlreadyAcquiredException", "GitHub.DistributedTask.WebApi.TaskOrchestrationJobAlreadyAcquiredException, GitHub.DistributedTask.WebApi, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public sealed class TaskOrchestrationJobAlreadyAcquiredException : DistributedTaskException
{
public TaskOrchestrationJobAlreadyAcquiredException(String message)
: base(message)
{
}
public TaskOrchestrationJobAlreadyAcquiredException(String message, Exception innerException)
: base(message, innerException)
{
}
private TaskOrchestrationJobAlreadyAcquiredException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
[Serializable]
[ExceptionMapping("0.0", "3.0", "TaskOrchestrationPlanSecurityException", "GitHub.DistributedTask.WebApi.TaskOrchestrationPlanSecurityException, GitHub.DistributedTask.WebApi, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public sealed class TaskOrchestrationPlanSecurityException : DistributedTaskException

View File

@@ -1,7 +1,8 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using GitHub.Services.Common;
namespace GitHub.DistributedTask.WebApi
{
@@ -9,78 +10,69 @@ namespace GitHub.DistributedTask.WebApi
public sealed class TimelineRecord
{
public TimelineRecord()
: this(null)
{
this.Attempt = 1;
}
private TimelineRecord(TimelineRecord recordToBeCloned)
{
this.EnsureInitialized();
this.Attempt = recordToBeCloned.Attempt;
this.ChangeId = recordToBeCloned.ChangeId;
this.CurrentOperation = recordToBeCloned.CurrentOperation;
this.FinishTime = recordToBeCloned.FinishTime;
this.Id = recordToBeCloned.Id;
this.Identifier = recordToBeCloned.Identifier;
this.LastModified = recordToBeCloned.LastModified;
this.Location = recordToBeCloned.Location;
this.Name = recordToBeCloned.Name;
this.Order = recordToBeCloned.Order;
this.ParentId = recordToBeCloned.ParentId;
this.PercentComplete = recordToBeCloned.PercentComplete;
this.RecordType = recordToBeCloned.RecordType;
this.Result = recordToBeCloned.Result;
this.ResultCode = recordToBeCloned.ResultCode;
this.StartTime = recordToBeCloned.StartTime;
this.State = recordToBeCloned.State;
this.TimelineId = recordToBeCloned.TimelineId;
this.WorkerName = recordToBeCloned.WorkerName;
this.RefName = recordToBeCloned.RefName;
this.ErrorCount = recordToBeCloned.ErrorCount;
this.WarningCount = recordToBeCloned.WarningCount;
this.NoticeCount = recordToBeCloned.NoticeCount;
this.AgentPlatform = recordToBeCloned.AgentPlatform;
if (recordToBeCloned != null)
if (recordToBeCloned.Log != null)
{
this.Attempt = recordToBeCloned.Attempt;
this.ChangeId = recordToBeCloned.ChangeId;
this.CurrentOperation = recordToBeCloned.CurrentOperation;
this.FinishTime = recordToBeCloned.FinishTime;
this.Id = recordToBeCloned.Id;
this.Identifier = recordToBeCloned.Identifier;
this.LastModified = recordToBeCloned.LastModified;
this.Location = recordToBeCloned.Location;
this.Name = recordToBeCloned.Name;
this.Order = recordToBeCloned.Order;
this.ParentId = recordToBeCloned.ParentId;
this.PercentComplete = recordToBeCloned.PercentComplete;
this.RecordType = recordToBeCloned.RecordType;
this.Result = recordToBeCloned.Result;
this.ResultCode = recordToBeCloned.ResultCode;
this.StartTime = recordToBeCloned.StartTime;
this.State = recordToBeCloned.State;
this.TimelineId = recordToBeCloned.TimelineId;
this.WorkerName = recordToBeCloned.WorkerName;
this.RefName = recordToBeCloned.RefName;
this.ErrorCount = recordToBeCloned.ErrorCount;
this.WarningCount = recordToBeCloned.WarningCount;
this.NoticeCount = recordToBeCloned.NoticeCount;
this.AgentPlatform = recordToBeCloned.AgentPlatform;
if (recordToBeCloned.Log != null)
this.Log = new TaskLogReference
{
this.Log = new TaskLogReference
{
Id = recordToBeCloned.Log.Id,
Location = recordToBeCloned.Log.Location,
};
}
Id = recordToBeCloned.Log.Id,
Location = recordToBeCloned.Log.Location,
};
}
if (recordToBeCloned.Details != null)
if (recordToBeCloned.Details != null)
{
this.Details = new TimelineReference
{
this.Details = new TimelineReference
{
ChangeId = recordToBeCloned.Details.ChangeId,
Id = recordToBeCloned.Details.Id,
Location = recordToBeCloned.Details.Location,
};
}
ChangeId = recordToBeCloned.Details.ChangeId,
Id = recordToBeCloned.Details.Id,
Location = recordToBeCloned.Details.Location,
};
}
if (recordToBeCloned.m_issues?.Count > 0)
{
this.Issues.AddRange(recordToBeCloned.Issues.Select(i => i.Clone()));
}
if (recordToBeCloned.m_issues?.Count> 0)
{
this.Issues.AddRange(recordToBeCloned.Issues.Select(i => i.Clone()));
}
if (recordToBeCloned.m_previousAttempts?.Count > 0)
{
this.m_previousAttempts.AddRange(recordToBeCloned.m_previousAttempts);
}
if (recordToBeCloned.m_previousAttempts?.Count > 0)
{
this.PreviousAttempts.AddRange(recordToBeCloned.PreviousAttempts);
}
if (recordToBeCloned.m_variables?.Count > 0)
{
// Don't pave over the case-insensitive Dictionary we initialized above.
foreach (var kvp in recordToBeCloned.m_variables)
{
m_variables[kvp.Key] = kvp.Value.Clone();
}
}
if (recordToBeCloned.m_variables?.Count > 0)
{
this.m_variables = recordToBeCloned.Variables.ToDictionary(k => k.Key, v => v.Value.Clone());
}
}
@@ -106,14 +98,14 @@ namespace GitHub.DistributedTask.WebApi
}
[DataMember(Name = "Type", Order = 3)]
public string RecordType
public String RecordType
{
get;
set;
}
[DataMember(Order = 4)]
public string Name
public String Name
{
get;
set;
@@ -134,7 +126,7 @@ namespace GitHub.DistributedTask.WebApi
}
[DataMember(Order = 7)]
public string CurrentOperation
public String CurrentOperation
{
get;
set;
@@ -162,7 +154,7 @@ namespace GitHub.DistributedTask.WebApi
}
[DataMember(Order = 11)]
public string ResultCode
public String ResultCode
{
get;
set;
@@ -183,7 +175,7 @@ namespace GitHub.DistributedTask.WebApi
}
[DataMember(Order = 14)]
public string WorkerName
public String WorkerName
{
get;
set;
@@ -197,7 +189,7 @@ namespace GitHub.DistributedTask.WebApi
}
[DataMember(Order = 16, EmitDefaultValue = false)]
public string RefName
public String RefName
{
get;
set;
@@ -217,46 +209,35 @@ namespace GitHub.DistributedTask.WebApi
set;
}
public Int32 ErrorCount
[DataMember(Order = 40)]
public Int32? ErrorCount
{
get
{
return m_errorCount.GetValueOrDefault(0);
}
set
{
m_errorCount = value;
}
get;
set;
}
public Int32 WarningCount
[DataMember(Order = 50)]
public Int32? WarningCount
{
get
{
return m_warningCount.GetValueOrDefault(0);
}
set
{
m_warningCount = value;
}
get;
set;
}
public Int32 NoticeCount
[DataMember(Order = 55)]
public Int32? NoticeCount
{
get
{
return m_noticeCount.GetValueOrDefault(0);
}
set
{
m_noticeCount = value;
}
get;
set;
}
public List<Issue> Issues
{
get
{
if (m_issues == null)
{
m_issues = new List<Issue>();
}
return m_issues;
}
}
@@ -276,7 +257,7 @@ namespace GitHub.DistributedTask.WebApi
}
[DataMember(Order = 131)]
public string Identifier
public String Identifier
{
get;
set;
@@ -293,14 +274,22 @@ namespace GitHub.DistributedTask.WebApi
{
get
{
if (m_previousAttempts == null)
{
m_previousAttempts = new List<TimelineAttempt>();
}
return m_previousAttempts;
}
}
public IDictionary<string, VariableValue> Variables
public IDictionary<String, VariableValue> Variables
{
get
{
if (m_variables == null)
{
m_variables = new Dictionary<String, VariableValue>(StringComparer.OrdinalIgnoreCase);
}
return m_variables;
}
}
@@ -310,53 +299,13 @@ namespace GitHub.DistributedTask.WebApi
return new TimelineRecord(this);
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
this.EnsureInitialized();
}
/// <summary>
/// DataContractSerializer bypasses all constructor logic and inline initialization!
/// This method takes the place of a workhorse constructor for baseline initialization.
/// The expectation is for this logic to be accessible to constructors and also to the OnDeserialized helper.
/// </summary>
private void EnsureInitialized()
{
// Note that ?? is a short-circuiting operator. (??= would be preferable, but it's not supported in the .NET Framework version currently used by actions/runner.)
// De-nullify the following historically-nullable ints.
// (After several weeks in production, it may be possible to eliminate these nullable backing fields.)
m_errorCount = m_errorCount ?? 0;
m_warningCount = m_warningCount ?? 0;
m_noticeCount = m_noticeCount ?? 0;
m_issues = m_issues ?? new List<Issue>();
m_previousAttempts = m_previousAttempts ?? new List<TimelineAttempt>();
this.Attempt = Math.Max(this.Attempt, 1);
// Ensure whatever content may have been deserialized for m_variables is backed by a case-insensitive Dictionary.
var empty = Enumerable.Empty<KeyValuePair<string, VariableValue>>();
m_variables = new Dictionary<string, VariableValue>(m_variables ?? empty, StringComparer.OrdinalIgnoreCase);
}
[DataMember(Name = nameof(ErrorCount), Order = 40)]
private Int32? m_errorCount;
[DataMember(Name = nameof(WarningCount), Order = 50)]
private Int32? m_warningCount;
[DataMember(Name = nameof(NoticeCount), Order = 55)]
private Int32? m_noticeCount;
[DataMember(Name = nameof(Issues), EmitDefaultValue = false, Order = 60)]
[DataMember(Name = "Issues", EmitDefaultValue = false, Order = 60)]
private List<Issue> m_issues;
[DataMember(Name = nameof(Variables), EmitDefaultValue = false, Order = 80)]
private Dictionary<string, VariableValue> m_variables;
[DataMember(Name = "Variables", EmitDefaultValue = false, Order = 80)]
private Dictionary<String, VariableValue> m_variables;
[DataMember(Name = nameof(PreviousAttempts), EmitDefaultValue = false, Order = 120)]
[DataMember(Name = "PreviousAttempts", EmitDefaultValue = false, Order = 120)]
private List<TimelineAttempt> m_previousAttempts;
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
namespace GitHub.DistributedTask.WebApi
{
@@ -6,6 +6,5 @@ namespace GitHub.DistributedTask.WebApi
{
public static readonly String JobId = "system.jobId";
public static readonly String RunnerLowDiskspaceThreshold = "system.runner.lowdiskspacethreshold";
public static readonly String RunnerEnvironment = "system.runnerEnvironment";
}
}

View File

@@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using GitHub.DistributedTask.WebApi;
namespace GitHub.Actions.RunService.WebApi
{
[DataContract]
public class AcquireJobRequest
{
[DataMember(Name = "jobMessageId", EmitDefaultValue = false)]
public string JobMessageId { get; set; }
[DataMember(Name = "streamId", EmitDefaultValue = false)]
public string StreamID { get; set; }
}
}
}

View File

@@ -1,35 +0,0 @@
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;
}
}

View File

@@ -1,20 +0,0 @@
using System.Runtime.Serialization;
namespace Sdk.RSWebApi.Contracts
{
[DataContract]
public enum AnnotationLevel
{
[EnumMember]
UNKNOWN = 0,
[EnumMember]
NOTICE = 1,
[EnumMember]
WARNING = 2,
[EnumMember]
FAILURE = 3
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using GitHub.DistributedTask.WebApi;
using Sdk.RSWebApi.Contracts;
namespace GitHub.Actions.RunService.WebApi
{
@@ -11,20 +10,17 @@ namespace GitHub.Actions.RunService.WebApi
{
[DataMember(Name = "planId", EmitDefaultValue = false)]
public Guid PlanID { get; set; }
[DataMember(Name = "jobId", EmitDefaultValue = false)]
public Guid JobID { get; set; }
[DataMember(Name = "conclusion")]
public TaskResult Conclusion { get; set; }
[DataMember(Name = "outputs", EmitDefaultValue = false)]
public Dictionary<string, VariableValue> Outputs { get; set; }
public Dictionary<string, VariableValue> Outputs { get; set; }
[DataMember(Name = "stepResults", EmitDefaultValue = false)]
public IList<StepResult> StepResults { get; set; }
[DataMember(Name = "annotations", EmitDefaultValue = false)]
public IList<Annotation> Annotations { get; set; }
}
}
}

View File

@@ -1,91 +0,0 @@
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;
}
}
}

View File

@@ -1,13 +0,0 @@
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";
}
}

View File

@@ -1,9 +1,7 @@
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
{
@@ -36,8 +34,5 @@ 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; }
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -56,7 +55,7 @@ namespace GitHub.Actions.RunService.WebApi
{
}
public async Task<AgentJobRequestMessage> GetJobMessageAsync(
public Task<AgentJobRequestMessage> GetJobMessageAsync(
Uri requestUri,
string messageId,
CancellationToken cancellationToken = default)
@@ -64,42 +63,26 @@ namespace GitHub.Actions.RunService.WebApi
HttpMethod httpMethod = new HttpMethod("POST");
var payload = new AcquireJobRequest
{
JobMessageId = messageId,
StreamID = messageId
};
requestUri = new Uri(requestUri, "acquirejob");
var requestContent = new ObjectContent<AcquireJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
var result = await SendAsync<AgentJobRequestMessage>(
return SendAsync<AgentJobRequestMessage>(
httpMethod,
requestUri: requestUri,
content: requestContent,
cancellationToken: cancellationToken);
if (result.IsSuccess)
{
return result.Value;
}
switch (result.StatusCode)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job message not found: {messageId}");
case HttpStatusCode.Conflict:
throw new TaskOrchestrationJobAlreadyAcquiredException($"Job message already acquired: {messageId}");
default:
throw new Exception($"Failed to get job message: {result.Error}");
}
}
public async Task CompleteJobAsync(
public Task CompleteJobAsync(
Uri requestUri,
Guid planId,
Guid jobId,
TaskResult result,
Dictionary<String, VariableValue> outputs,
IList<StepResult> stepResults,
IList<Annotation> jobAnnotations,
CancellationToken cancellationToken = default)
{
HttpMethod httpMethod = new HttpMethod("POST");
@@ -109,33 +92,20 @@ namespace GitHub.Actions.RunService.WebApi
JobID = jobId,
Conclusion = result,
Outputs = outputs,
StepResults = stepResults,
Annotations = jobAnnotations
StepResults = stepResults
};
requestUri = new Uri(requestUri, "completejob");
var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
var response = await SendAsync(
return SendAsync(
httpMethod,
requestUri,
content: requestContent,
cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode)
{
return;
}
switch (response.StatusCode)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}");
default:
throw new Exception($"Failed to complete job: {response.ReasonPhrase}");
}
}
public async Task<RenewJobResponse> RenewJobAsync(
public Task<RenewJobResponse> RenewJobAsync(
Uri requestUri,
Guid planId,
Guid jobId,
@@ -151,24 +121,11 @@ namespace GitHub.Actions.RunService.WebApi
requestUri = new Uri(requestUri, "renewjob");
var requestContent = new ObjectContent<RenewJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
var result = await SendAsync<RenewJobResponse>(
return SendAsync<RenewJobResponse>(
httpMethod,
requestUri,
content: requestContent,
cancellationToken: cancellationToken);
if (result.IsSuccess)
{
return result.Value;
}
switch (result.StatusCode)
{
case HttpStatusCode.NotFound:
throw new TaskOrchestrationJobNotFoundException($"Job not found: {jobId}");
default:
throw new Exception($"Failed to renew job: {result.Error}");
}
}
}
}

View File

@@ -8,7 +8,7 @@
<NoWarn>NU1701;NU1603</NoWarn>
<Version>$(Version)</Version>
<DefineConstants>TRACE</DefineConstants>
<LangVersion>8.0</LangVersion>
<LangVersion>7.3</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -56,7 +55,7 @@ namespace GitHub.Actions.RunService.WebApi
{
}
public async Task<TaskAgentMessage> GetRunnerMessageAsync(
public Task<TaskAgentMessage> GetRunnerMessageAsync(
string runnerVersion,
TaskAgentStatus? status,
CancellationToken cancellationToken = default
@@ -75,18 +74,11 @@ namespace GitHub.Actions.RunService.WebApi
queryParams.Add("runnerVersion", runnerVersion);
}
var result = await SendAsync<TaskAgentMessage>(
return SendAsync<TaskAgentMessage>(
new HttpMethod("GET"),
requestUri: requestUri,
queryParameters: queryParams,
cancellationToken: cancellationToken);
if (result.IsSuccess)
{
return result.Value;
}
throw new Exception($"Failed to get job message: {result.Error}");
}
}
}

View File

@@ -157,8 +157,6 @@ namespace GitHub.Services.Results.Contracts
public string StartedAt;
[DataMember]
public string CompletedAt;
[DataMember]
public Conclusion Conclusion;
}
public enum Status
@@ -169,15 +167,6 @@ namespace GitHub.Services.Results.Contracts
StatusCompleted = 6
}
public enum Conclusion
{
ConclusionUnknown = 0,
ConclusionSuccess = 2,
ConclusionFailure = 3,
ConclusionCancelled = 4,
ConclusionSkipped = 7,
}
public static class BlobStorageTypes
{
public static readonly string AzureBlobStorage = "BLOB_STORAGE_TYPE_AZURE";

View File

@@ -1,70 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace GitHub.Services.Launch.Contracts
{
[DataContract]
public class ActionReferenceRequest
{
[DataMember(EmitDefaultValue = false, Name = "action")]
public string Action { get; set; }
[DataMember(EmitDefaultValue = false, Name = "version")]
public string Version { get; set; }
[DataMember(EmitDefaultValue = false, Name = "path")]
public string Path { get; set; }
}
[DataContract]
public class ActionReferenceRequestList
{
[DataMember(EmitDefaultValue = false, Name = "actions")]
public IList<ActionReferenceRequest> Actions { get; set; }
}
[DataContract]
public class ActionDownloadInfoResponse
{
[DataMember(EmitDefaultValue = false, Name = "authentication")]
public ActionDownloadAuthenticationResponse Authentication { get; set; }
[DataMember(EmitDefaultValue = false, Name = "name")]
public string Name { get; set; }
[DataMember(EmitDefaultValue = false, Name = "resolved_name")]
public string ResolvedName { get; set; }
[DataMember(EmitDefaultValue = false, Name = "resolved_sha")]
public string ResolvedSha { get; set; }
[DataMember(EmitDefaultValue = false, Name = "tar_url")]
public string TarUrl { get; set; }
[DataMember(EmitDefaultValue = false, Name = "version")]
public string Version { get; set; }
[DataMember(EmitDefaultValue = false, Name = "zip_url")]
public string ZipUrl { get; set; }
}
[DataContract]
public class ActionDownloadAuthenticationResponse
{
[DataMember(EmitDefaultValue = false, Name = "expires_at")]
public DateTime ExpiresAt { get; set; }
[DataMember(EmitDefaultValue = false, Name = "token")]
public string Token { get; set; }
}
[DataContract]
public class ActionDownloadInfoResponseCollection
{
/// <summary>A mapping of action specifications to their download information.</summary>
/// <remarks>The key is the full name of the action plus version, e.g. "actions/checkout@v2".</remarks>
[DataMember(EmitDefaultValue = false, Name = "actions")]
public IDictionary<string, ActionDownloadInfoResponse> Actions { get; set; }
}
}

View File

@@ -1,115 +0,0 @@
#nullable enable
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Services.Launch.Contracts;
using Sdk.WebApi.WebApi;
namespace GitHub.Services.Launch.Client
{
public class LaunchHttpClient : RawHttpClientBase
{
public LaunchHttpClient(
Uri baseUrl,
HttpMessageHandler pipeline,
string token,
bool disposeHandler)
: base(baseUrl, pipeline, disposeHandler)
{
m_token = token;
m_launchServiceUrl = baseUrl;
m_formatter = new JsonMediaTypeFormatter();
}
public async Task<ActionDownloadInfoCollection> GetResolveActionsDownloadInfoAsync(Guid planId, Guid jobId, ActionReferenceList actionReferenceList, CancellationToken cancellationToken)
{
var GetResolveActionsDownloadInfoURLEndpoint = new Uri(m_launchServiceUrl, $"/actions/build/{planId.ToString()}/jobs/{jobId.ToString()}/runnerresolve/actions");
return ToServerData(await GetLaunchSignedURLResponse<ActionReferenceRequestList, ActionDownloadInfoResponseCollection>(GetResolveActionsDownloadInfoURLEndpoint, ToGitHubData(actionReferenceList), cancellationToken));
}
// Resolve Actions
private async Task<T> GetLaunchSignedURLResponse<R, T>(Uri uri, R request, CancellationToken cancellationToken)
{
using (HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Post, uri))
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", m_token);
requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
using (HttpContent content = new ObjectContent<R>(request, m_formatter))
{
requestMessage.Content = content;
using (var response = await SendAsync(requestMessage, HttpCompletionOption.ResponseContentRead, cancellationToken: cancellationToken))
{
return await ReadJsonContentAsync<T>(response, cancellationToken);
}
}
}
}
private static ActionReferenceRequestList ToGitHubData(ActionReferenceList actionReferenceList)
{
return new ActionReferenceRequestList
{
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList()
};
}
private static ActionReferenceRequest ToGitHubData(ActionReference actionReference)
{
return new ActionReferenceRequest
{
Action = actionReference.NameWithOwner,
Version = actionReference.Ref,
Path = actionReference.Path
};
}
private static ActionDownloadInfoCollection ToServerData(ActionDownloadInfoResponseCollection actionDownloadInfoResponseCollection)
{
return new ActionDownloadInfoCollection
{
Actions = actionDownloadInfoResponseCollection.Actions?.ToDictionary(kvp => kvp.Key, kvp => ToServerData(kvp.Value))
};
}
private static ActionDownloadInfo ToServerData(ActionDownloadInfoResponse actionDownloadInfoResponse)
{
return new ActionDownloadInfo
{
Authentication = ToServerData(actionDownloadInfoResponse.Authentication),
NameWithOwner = actionDownloadInfoResponse.Name,
ResolvedNameWithOwner = actionDownloadInfoResponse.ResolvedName,
ResolvedSha = actionDownloadInfoResponse.ResolvedSha,
TarballUrl = actionDownloadInfoResponse.TarUrl,
Ref = actionDownloadInfoResponse.Version,
ZipballUrl = actionDownloadInfoResponse.ZipUrl,
};
}
private static ActionDownloadAuthentication? ToServerData(ActionDownloadAuthenticationResponse? actionDownloadAuthenticationResponse)
{
if (actionDownloadAuthenticationResponse == null)
{
return null;
}
return new ActionDownloadAuthentication
{
ExpiresAt = actionDownloadAuthenticationResponse.ExpiresAt,
Token = actionDownloadAuthenticationResponse.Token
};
}
private MediaTypeFormatter m_formatter;
private Uri m_launchServiceUrl;
private string m_token;
}
}

View File

@@ -21,7 +21,7 @@ using Newtonsoft.Json.Linq;
namespace Sdk.WebApi.WebApi
{
public class RawHttpClientBase : IDisposable
public class RawHttpClientBase: IDisposable
{
protected RawHttpClientBase(
Uri baseUrl,
@@ -101,7 +101,7 @@ namespace Sdk.WebApi.WebApi
}
}
protected Task<RawHttpClientResult<T>> SendAsync<T>(
protected Task<T> SendAsync<T>(
HttpMethod method,
Uri requestUri,
HttpContent content = null,
@@ -112,7 +112,7 @@ namespace Sdk.WebApi.WebApi
return SendAsync<T>(method, null, requestUri, content, queryParameters, userState, cancellationToken);
}
protected async Task<RawHttpClientResult<T>> SendAsync<T>(
protected async Task<T> SendAsync<T>(
HttpMethod method,
IEnumerable<KeyValuePair<String, String>> additionalHeaders,
Uri requestUri,
@@ -128,7 +128,7 @@ namespace Sdk.WebApi.WebApi
}
}
protected async Task<RawHttpClientResult<T>> SendAsync<T>(
protected async Task<T> SendAsync<T>(
HttpRequestMessage message,
Object userState = null,
CancellationToken cancellationToken = default(CancellationToken))
@@ -138,16 +138,7 @@ namespace Sdk.WebApi.WebApi
//from deadlocking...
using (HttpResponseMessage response = await this.SendAsync(message, userState, cancellationToken).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
T data = await ReadContentAsAsync<T>(response, cancellationToken).ConfigureAwait(false);
return RawHttpClientResult<T>.Ok(data);
}
else
{
string errorMessage = $"Error: {response.ReasonPhrase}";
return RawHttpClientResult<T>.Fail(errorMessage, response.StatusCode);
}
return await ReadContentAsAsync<T>(response, cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -1,33 +0,0 @@
using System.Net;
namespace Sdk.WebApi.WebApi
{
public class RawHttpClientResult
{
public bool IsSuccess { get; protected set; }
public string Error { get; protected set; }
public HttpStatusCode StatusCode { get; protected set; }
public bool IsFailure => !IsSuccess;
protected RawHttpClientResult(bool isSuccess, string error, HttpStatusCode statusCode)
{
IsSuccess = isSuccess;
Error = error;
StatusCode = statusCode;
}
}
public class RawHttpClientResult<T> : RawHttpClientResult
{
public T Value { get; private set; }
protected internal RawHttpClientResult(T value, bool isSuccess, string error, HttpStatusCode statusCode)
: base(isSuccess, error, statusCode)
{
Value = value;
}
public static RawHttpClientResult<T> Fail(string message, HttpStatusCode statusCode) => new RawHttpClientResult<T>(default(T), false, message, statusCode);
public static RawHttpClientResult<T> Ok(T value) => new RawHttpClientResult<T>(value, true, string.Empty, HttpStatusCode.OK);
}
}

View File

@@ -329,8 +329,7 @@ namespace GitHub.Services.Results.Client
Name = r.Name,
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat),
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat),
Conclusion = ConvertResultToConclusion(r.Result)
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat)
};
}
@@ -349,29 +348,6 @@ namespace GitHub.Services.Results.Client
}
}
private Conclusion ConvertResultToConclusion(TaskResult? r)
{
if (!r.HasValue)
{
return Conclusion.ConclusionUnknown;
}
switch (r)
{
case TaskResult.Succeeded:
case TaskResult.SucceededWithIssues:
return Conclusion.ConclusionSuccess;
case TaskResult.Canceled:
return Conclusion.ConclusionCancelled;
case TaskResult.Skipped:
return Conclusion.ConclusionSkipped;
case TaskResult.Failed:
return Conclusion.ConclusionFailure;
default:
return Conclusion.ConclusionUnknown;
}
}
public async Task UpdateWorkflowStepsAsync(Guid planId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken)
{
var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat);

View File

@@ -1,270 +0,0 @@
#nullable enable
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization.Json;
using GitHub.DistributedTask.WebApi;
using Xunit;
using System.Text;
namespace GitHub.Runner.Common.Tests.DistributedTask
{
public sealed class TimelineRecordL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_Defaults()
{
var tr = new TimelineRecord();
Assert.Equal(0, tr.ErrorCount);
Assert.Equal(0, tr.WarningCount);
Assert.Equal(0, tr.NoticeCount);
Assert.Equal(1, tr.Attempt);
Assert.NotNull(tr.Issues);
Assert.NotNull(tr.PreviousAttempts);
Assert.NotNull(tr.Variables);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_Clone()
{
var original = new TimelineRecord();
original.ErrorCount = 100;
original.WarningCount = 200;
original.NoticeCount = 300;
original.Attempt = 3;
// The Variables dictionary should be a case-insensitive dictionary.
original.Variables["xxx"] = new VariableValue("first", false);
original.Variables["XXX"] = new VariableValue("second", false);
Assert.Equal(1, original.Variables.Count);
Assert.Equal("second", original.Variables.Values.First().Value);
Assert.Equal("second", original.Variables["xXx"].Value);
var clone = original.Clone();
Assert.NotSame(original, clone);
Assert.NotSame(original.Variables, clone.Variables);
Assert.Equal(100, clone.ErrorCount);
Assert.Equal(200, clone.WarningCount);
Assert.Equal(300, clone.NoticeCount);
Assert.Equal(3, clone.Attempt);
// Now, mutate the original post-clone.
original.ErrorCount++;
original.WarningCount += 10;
original.NoticeCount *= 3;
original.Attempt--;
original.Variables["a"] = new VariableValue("1", false);
// Verify that the clone was unaffected by the changes to the original.
Assert.Equal(100, clone.ErrorCount);
Assert.Equal(200, clone.WarningCount);
Assert.Equal(300, clone.NoticeCount);
Assert.Equal(3, clone.Attempt);
Assert.Equal(1, clone.Variables.Count);
Assert.Equal("second", clone.Variables.Values.First().Value);
// Verify that the clone's Variables dictionary is also case-sensitive.
clone.Variables["yyy"] = new VariableValue("third", false);
clone.Variables["YYY"] = new VariableValue("fourth", false);
Assert.Equal(2, clone.Variables.Count);
Assert.Equal("second", clone.Variables["xXx"].Value);
Assert.Equal("fourth", clone.Variables["yYy"].Value);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_DeserializationEdgeCase_NonNullCollections()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
// Verify that missing JSON fields don't result in null values for collection properties.
var tr = Deserialize(jsonSamples["minimal"]);
Assert.NotNull(tr);
Assert.Equal("minimal", tr!.Name);
Assert.NotNull(tr.Issues);
Assert.NotNull(tr.PreviousAttempts);
Assert.NotNull(tr.Variables);
// Verify that explicitly-null JSON fields don't result in null values for collection properties.
// (Our deserialization logic should fix these up and instantiate an empty collection.)
tr = Deserialize(jsonSamples["explicit-null-collections"]);
Assert.NotNull(tr);
Assert.Equal("explicit-null-collections", tr!.Name);
Assert.NotNull(tr.Issues);
Assert.NotNull(tr.PreviousAttempts);
Assert.NotNull(tr.Variables);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_DeserializationEdgeCase_AttemptCannotBeLessThan1()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
// Verify that 1 is the effective floor for TimelineRecord::Attempt.
var tr = Deserialize(jsonSamples["minimal"]);
Assert.NotNull(tr);
Assert.Equal("minimal", tr!.Name);
Assert.Equal(1, tr.Attempt);
tr = Deserialize(jsonSamples["invalid-attempt-value"]);
Assert.NotNull(tr);
Assert.Equal("invalid-attempt-value", tr!.Name);
Assert.Equal(1, tr.Attempt);
tr = Deserialize(jsonSamples["zero-attempt-value"]);
Assert.NotNull(tr);
Assert.Equal("zero-attempt-value", tr!.Name);
Assert.Equal(1, tr.Attempt);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_DeserializationEdgeCase_HandleLegacyNullsGracefully()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
// Verify that nulls for ErrorCount, WarningCount, and NoticeCount are interpreted as 0.
var tr = Deserialize(jsonSamples["legacy-nulls"]);
Assert.NotNull(tr);
Assert.Equal("legacy-nulls", tr!.Name);
Assert.Equal(0, tr.ErrorCount);
Assert.Equal(0, tr.WarningCount);
Assert.Equal(0, tr.NoticeCount);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_DeserializationEdgeCase_HandleMissingCountsGracefully()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
// Verify that nulls for ErrorCount, WarningCount, and NoticeCount are interpreted as 0.
var tr = Deserialize(jsonSamples["missing-counts"]);
Assert.NotNull(tr);
Assert.Equal("missing-counts", tr!.Name);
Assert.Equal(0, tr.ErrorCount);
Assert.Equal(0, tr.WarningCount);
Assert.Equal(0, tr.NoticeCount);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_DeserializationEdgeCase_NonZeroCounts()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
// Verify that nulls for ErrorCount, WarningCount, and NoticeCount are interpreted as 0.
var tr = Deserialize(jsonSamples["non-zero-counts"]);
Assert.NotNull(tr);
Assert.Equal("non-zero-counts", tr!.Name);
Assert.Equal(10, tr.ErrorCount);
Assert.Equal(20, tr.WarningCount);
Assert.Equal(30, tr.NoticeCount);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_Deserialization_LeanTimelineRecord()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
// Verify that a lean TimelineRecord can be deserialized.
var tr = Deserialize(jsonSamples["lean"]);
Assert.NotNull(tr);
Assert.Equal("lean", tr!.Name);
Assert.Equal(4, tr.Attempt);
Assert.Equal(1, tr.Issues.Count);
Assert.Equal(3, tr.Variables.Count);
Assert.Equal(3, tr.PreviousAttempts.Count);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_Deserialization_VariablesDictionaryIsCaseInsensitive()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
var tr = Deserialize(jsonSamples["lean"]);
Assert.NotNull(tr);
Assert.Equal("lean", tr!.Name);
Assert.Equal(3, tr.Variables.Count);
// Verify that the Variables Dictionary is case-insensitive.
tr.Variables["X"] = new VariableValue("overwritten", false);
Assert.Equal(3, tr.Variables.Count);
tr.Variables["new"] = new VariableValue("new.1", false);
Assert.Equal(4, tr.Variables.Count);
tr.Variables["NEW"] = new VariableValue("new.2", false);
Assert.Equal(4, tr.Variables.Count);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "DistributedTask")]
public void VerifyTimelineRecord_DeserializationEdgeCase_DuplicateVariableKeysThrowsException()
{
var jsonSamples = LoadJsonSamples(JsonSamplesFilePath);
// We could be more forgiving in this case if we discover that it's not uncommon in Production for serialized TimelineRecords to:
// 1) get incorrectly instantiated with a case-sensitive Variables dictionary (in older versions, this was possible via TimelineRecord::Clone)
// 2) end up with case variations of the same key
// 3) make another serialization/deserialization round trip.
//
// If we wanted to grant clemency to such incorrectly-serialized TimelineRecords,
// the fix to TimelineRecord::EnsureInitialized would look something like the following:
//
// var seedVariables = m_variables ?? Enumerable.Empty<KeyValuePair<string, VariableValue>>();
// m_variables = new Dictionary<string, VariableValue>(seedVariables.Count(), StringComparer.OrdinalIgnoreCase);
// foreach (var kvp in seedVariables)
// {
// m_variables[kvp.Key] = kvp.Value;
// }
Assert.Throws<ArgumentException>(() => Deserialize(jsonSamples["duplicate-variable-keys"]));
}
private static Dictionary<string, string> LoadJsonSamples(string path)
{
// Embedding independent JSON samples within YML works well because JSON generally doesn't need to be escaped or otherwise mangled.
var yamlDeserializer = new YamlDotNet.Serialization.Deserializer();
using var stream = new StreamReader(path);
return yamlDeserializer.Deserialize<Dictionary<string, string>>(stream);
}
private static TimelineRecord? Deserialize(string rawJson)
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(rawJson ?? string.Empty));
return m_jsonSerializer.ReadObject(stream) as TimelineRecord;
}
private static string JsonSamplesFilePath
{
get
{
return Path.Combine(TestUtil.GetTestDataPath(), "timelinerecord_json_samples.yml");
}
}
private static readonly DataContractJsonSerializer m_jsonSerializer = new(typeof(TimelineRecord));
}
}

View File

@@ -1,4 +1,3 @@
using GitHub.Runner.Common.Util;
using System;
using System.IO;
using System.Reflection;
@@ -13,6 +12,7 @@ namespace GitHub.Runner.Common.Tests
{
private HostContext _hc;
private CancellationTokenSource _tokenSource;
private const string EXPECTED_SECRET_MASK = "***";
[Fact]
[Trait("Level", "L0")]
@@ -95,11 +95,11 @@ namespace GitHub.Runner.Common.Tests
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass%20word%20123%21123"));
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass&lt;word&gt;123!123"));
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Pass''word''123!123"));
Assert.Equal("OlBh***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($":Password123!"))));
Assert.Equal("YTpQ***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"a:Password123!"))));
Assert.Equal("OlBh***==", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($":Password123!"))));
Assert.Equal("YTpQ***=", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"a:Password123!"))));
Assert.Equal("YWI6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"ab:Password123!"))));
Assert.Equal("YWJjOlBh***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abc:Password123!"))));
Assert.Equal("YWJjZDpQ***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcd:Password123!"))));
Assert.Equal("YWJjOlBh***==", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abc:Password123!"))));
Assert.Equal("YWJjZDpQ***=", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcd:Password123!"))));
Assert.Equal("YWJjZGU6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcde:Password123!"))));
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!!123"));
Assert.Equal("123short123", _hc.SecretMasker.MaskSecrets("123short123"));
@@ -112,6 +112,116 @@ namespace GitHub.Runner.Common.Tests
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void Base64SecretMaskers()
{
// The following are good candidate strings for Base64 encoding because they include
// both standard and RFC 4648 Base64 digits in all offset variations.
// TeLL? noboDy~ SEcreT?
// tElL~ NEVER~ neveR?
// TIGht? Tight~ guard~
// pRIVAte~ guARd? TIghT~
// KeeP~ TIgHT? tIgHT~
// LoCk? TiGhT~ TIght~
// DIvULGe~ nObODY~ noBOdy?
// foreVER~ Tight~ GUaRd?
try
{
// Arrange.
Setup();
// Act.
_hc.SecretMasker.AddValue("TeLL? noboDy~ SEcreT?");
// The above string has the following Base64 variations based on the chop leading byte(s) method of Base64 aliasing:
var base64Variations = new[]
{
"VGVMTD8gbm9ib0R5fiBTRWNyZVQ/",
"ZUxMPyBub2JvRHl+IFNFY3JlVD8",
"TEw/IG5vYm9EeX4gU0VjcmVUPw",
// RFC 4648 (URL-safe Base64)
"VGVMTD8gbm9ib0R5fiBTRWNyZVQ_",
"ZUxMPyBub2JvRHl-IFNFY3JlVD8",
"TEw_IG5vYm9EeX4gU0VjcmVUPw"
};
var bookends = new[]
{
(string.Empty, string.Empty),
(string.Empty, "="),
(string.Empty, "=="),
(string.Empty, "==="),
("a", "z"),
("A", "Z"),
("abc", "abc"),
("ABC", "ABC"),
("0", "0"),
("00", "00"),
("000", "000"),
("123", "789"),
("`", "`"),
("'", "'"),
("\"", "\""),
("[", "]"),
("(", ")"),
("$(", ")"),
("{", "}"),
("${", "}"),
("!", "!"),
("!!", "!!"),
("%", "%"),
("%%", "%%"),
("_", "_"),
("__", "__"),
(":", ":"),
("::", "::"),
(";", ";"),
(";;", ";;"),
(":", string.Empty),
(";", string.Empty),
(string.Empty, ":"),
(string.Empty, ";"),
("VGVMTD8gbm9ib", "ZUxMPy"),
("VGVMTD8gbm9ib", "TEw/IG5vYm9EeX4"),
("ZUxMPy", "TEw/IG5vYm9EeX4"),
("VGVMTD8gbm9ib", string.Empty),
("TEw/IG5vYm9EeX4", string.Empty),
("ZUxMPy", string.Empty),
(string.Empty, "VGVMTD8gbm9ib"),
(string.Empty, "TEw/IG5vYm9EeX4"),
(string.Empty, "ZUxMPy"),
};
foreach (var variation in base64Variations)
{
foreach (var pair in bookends)
{
var (prefix, suffix) = pair;
var expected = string.Format("{0}{1}{2}", prefix, EXPECTED_SECRET_MASK, suffix);
var payload = string.Format("{0}{1}{2}", prefix, variation, suffix);
Assert.Equal(expected, _hc.SecretMasker.MaskSecrets(payload));
}
// Verify no masking is performed on a partial match.
for (int i = 1; i < variation.Length - 1; i++)
{
var fragment = variation[..i];
Assert.Equal(fragment, _hc.SecretMasker.MaskSecrets(fragment));
}
}
}
finally
{
// Cleanup.
Teardown();
}
}
[Theory]
[InlineData("secret&secret&secret", "secret&secret&\x0033[96msecret\x0033[0m", "***\x0033[96m***\x0033[0m")]
[InlineData("secret&secret+secret", "secret&\x0033[96msecret+secret\x0033[0m", "***\x0033[96m***\x0033[0m")]

View File

@@ -206,107 +206,6 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "ConfigurationManagement")]
public async Task ConfigureErrorDefaultLabelsDisabledWithNoCustomLabels()
{
using (TestHostContext tc = CreateTestContext())
{
Tracing trace = tc.GetTrace();
trace.Info("Creating config manager");
IConfigurationManager configManager = new ConfigurationManager();
configManager.Initialize(tc);
trace.Info("Preparing command line arguments");
var command = new CommandSettings(
tc,
new[]
{
"configure",
"--url", _expectedServerUrl,
"--name", _expectedAgentName,
"--runnergroup", _secondRunnerGroupName,
"--work", _expectedWorkFolder,
"--auth", _expectedAuthType,
"--token", _expectedToken,
"--no-default-labels",
"--ephemeral",
"--disableupdate",
"--unattended",
});
trace.Info("Constructed.");
_store.Setup(x => x.IsConfigured()).Returns(false);
_configMgrAgentSettings = null;
trace.Info("Ensuring configure fails if default labels are disabled and no custom labels are set");
var ex = await Assert.ThrowsAsync<NotSupportedException>(() => configManager.ConfigureAsync(command));
Assert.Contains("--no-default-labels without specifying --labels is not supported", ex.Message);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "ConfigurationManagement")]
public async Task ConfigureDefaultLabelsDisabledWithCustomLabels()
{
using (TestHostContext tc = CreateTestContext())
{
Tracing trace = tc.GetTrace();
trace.Info("Creating config manager");
IConfigurationManager configManager = new ConfigurationManager();
configManager.Initialize(tc);
var userLabels = "userlabel1,userlabel2";
trace.Info("Preparing command line arguments");
var command = new CommandSettings(
tc,
new[]
{
"configure",
"--url", _expectedServerUrl,
"--name", _expectedAgentName,
"--runnergroup", _secondRunnerGroupName,
"--work", _expectedWorkFolder,
"--auth", _expectedAuthType,
"--token", _expectedToken,
"--labels", userLabels,
"--no-default-labels",
"--ephemeral",
"--disableupdate",
"--unattended",
});
trace.Info("Constructed.");
_store.Setup(x => x.IsConfigured()).Returns(false);
_configMgrAgentSettings = null;
trace.Info("Ensuring all the required parameters are available in the command line parameter");
await configManager.ConfigureAsync(command);
_store.Setup(x => x.IsConfigured()).Returns(true);
trace.Info("Configured, verifying all the parameter value");
var s = configManager.LoadSettings();
Assert.NotNull(s);
Assert.True(s.ServerUrl.Equals(_expectedServerUrl));
Assert.True(s.AgentName.Equals(_expectedAgentName));
Assert.True(s.PoolId.Equals(_secondRunnerGroupId));
Assert.True(s.WorkFolder.Equals(_expectedWorkFolder));
Assert.True(s.Ephemeral.Equals(true));
// validate GetAgentPoolsAsync gets called twice with automation pool type
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2));
var expectedLabels = userLabels.Split(",").ToList();
_runnerServer.Verify(x => x.AddAgentAsync(It.IsAny<int>(), It.Is<TaskAgent>(a => a.Labels.Select(x => x.Name).ToHashSet().SetEquals(expectedLabels))), Times.Once);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "ConfigurationManagement")]

View File

@@ -1,275 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using Xunit;
namespace GitHub.DistributedTask.ObjectTemplating.Tests
{
public sealed class TemplateContextL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithTokenAndException()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("(Line: 1, Col: 1): Exception of type 'System.Exception' was thrown.") };
context.Error(new StringToken(1, 1, 1, "some-token"), new System.Exception());
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithNullTokenAndException()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("System.Exception: Exception of type 'System.Exception' was thrown.") };
context.Error(null, new System.Exception());
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithTokenAndMessage()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("(Line: 1, Col: 1): message") };
context.Error(new StringToken(1, 1, 1, "some-token"), "message");
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithNullTokenAndMessage()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("message") };
context.Error(null, "message");
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithException()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("(Line: 2, Col: 3): Fatal template exception") };
List<string> expectedTracewriterErrors = new List<string> { "(Line: 2, Col: 3):" };
context.Error(1, 2, 3, new Exception("Fatal template exception"));
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
ListTraceWriter listTraceWriter = (ListTraceWriter)context.TraceWriter;
List<string> tracewriterErrors = listTraceWriter.GetErrors();
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
Assert.True(expectedTracewriterErrors.SequenceEqual(tracewriterErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithExceptionAndInnerException()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("(Line: 2, Col: 3): Fatal template exception"),
new TemplateValidationError("(Line: 2, Col: 3): Inner Exception") };
List<string> expectedTracewriterErrors = new List<string> { "(Line: 2, Col: 3):" };
Exception e = new Exception("Fatal template exception", new Exception("Inner Exception"));
context.Error(1, 2, 3, e);
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
ListTraceWriter listTraceWriter = (ListTraceWriter)context.TraceWriter;
List<string> tracewriterErrors = listTraceWriter.GetErrors();
Assert.Equal(2, templateValidationErrors.Count);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
Assert.True(expectedTracewriterErrors.SequenceEqual(tracewriterErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithLineNumbersAndMessage()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("(Line: 2, Col: 3): Fatal template exception") };
List<string> expectedTracewriterErrors = new List<string> { "(Line: 2, Col: 3): Fatal template exception" };
context.Error(1, 2, 3, "Fatal template exception");
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
ListTraceWriter listTraceWriter = (ListTraceWriter)context.TraceWriter;
List<string> tracewriterErrors = listTraceWriter.GetErrors();
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
Assert.True(expectedTracewriterErrors.SequenceEqual(tracewriterErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithNullLineNumbersAndMessage()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("Fatal template exception") };
List<string> expectedTracewriterErrors = new List<string> { "Fatal template exception" };
context.Error(null, null, null, "Fatal template exception");
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
ListTraceWriter listTraceWriter = (ListTraceWriter)context.TraceWriter;
List<string> tracewriterErrors = listTraceWriter.GetErrors();
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
Assert.True(expectedTracewriterErrors.SequenceEqual(tracewriterErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithLineNumbersAndException()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("(Line: 2, Col: 3): Fatal template exception") };
List<string> expectedTracewriterErrors = new List<string> { "(Line: 2, Col: 3):" };
context.Error(1, 2, 3, new Exception("Fatal template exception"));
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
ListTraceWriter listTraceWriter = (ListTraceWriter)context.TraceWriter;
List<string> tracewriterErrors = listTraceWriter.GetErrors();
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
Assert.True(expectedTracewriterErrors.SequenceEqual(tracewriterErrors));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyErrorWithNullLineNumbersAndException()
{
TemplateContext context = buildContext();
List<TemplateValidationError> expectedErrors = new List<TemplateValidationError> { new TemplateValidationError("System.Exception: Fatal template exception") };
List<string> expectedTracewriterErrors = new List<string> { "" };
context.Error(null, null, null, new Exception("Fatal template exception"));
List<TemplateValidationError> templateValidationErrors = toList(context.Errors.GetEnumerator());
ListTraceWriter listTraceWriter = (ListTraceWriter)context.TraceWriter;
List<string> tracewriterErrors = listTraceWriter.GetErrors();
Assert.Single(templateValidationErrors);
Assert.True(areEqual(expectedErrors, templateValidationErrors));
Assert.True(expectedTracewriterErrors.SequenceEqual(tracewriterErrors));
}
private TemplateContext buildContext()
{
return new TemplateContext
{
// CancellationToken = CancellationToken.None,
Errors = new TemplateValidationErrors(10, int.MaxValue), // Don't truncate error messages otherwise we might not scrub secrets correctly
Memory = new TemplateMemory(
maxDepth: 100,
maxEvents: 1000000,
maxBytes: 10 * 1024 * 1024),
Schema = null,
TraceWriter = new ListTraceWriter(),
};
}
private List<TemplateValidationError> toList(IEnumerator<TemplateValidationError> enumerator)
{
List<TemplateValidationError> result = new();
while (enumerator.MoveNext())
{
TemplateValidationError err = enumerator.Current;
result.Add(err);
}
return result;
}
private bool areEqual(List<TemplateValidationError> l1, List<TemplateValidationError> l2)
{
if (l1.Count != l2.Count) return false;
var twoLists = l1.Zip(l2, (l1Error, l2Error) => new { Elem1 = l1Error, Elem2 = l2Error });
foreach (var elem in twoLists)
{
if (elem.Elem1.Message != elem.Elem2.Message) return false;
if (elem.Elem1.Code != elem.Elem2.Code) return false;
}
return true;
}
internal sealed class ListTraceWriter : ITraceWriter
{
private List<string> errors = new();
private List<string> infoMessages = new();
private List<string> verboseMessages = new();
public void Error(string format, params object[] args)
{
errors.Add(string.Format(System.Globalization.CultureInfo.CurrentCulture, $"{format}", args));
}
public void Info(string format, params object[] args)
{
throw new NotImplementedException();
}
public void Verbose(string format, params object[] args)
{
throw new NotImplementedException();
}
public List<string> GetErrors()
{
return errors;
}
}
}
}

View File

@@ -1,61 +0,0 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using Xunit;
namespace GitHub.Actions.RunService.WebApi.Tests;
public sealed class AcquireJobRequestL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifySerialization()
{
var request = new AcquireJobRequest
{
JobMessageId = "1526919030369-33"
};
var serializer = new DataContractJsonSerializer(typeof(AcquireJobRequest));
using var stream = new MemoryStream();
serializer.WriteObject(stream, request);
stream.Position = 0;
using var reader = new StreamReader(stream, Encoding.UTF8);
string json = reader.ReadToEnd();
string expected = DoubleQuotify(string.Format("{{'jobMessageId':'{0}'}}", request.JobMessageId));
Assert.Equal(expected, json);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyDeserialization()
{
var serializer = new DataContractJsonSerializer(typeof(AcquireJobRequest));
var variations = new Dictionary<string, string>()
{
["{'streamId': 'legacy', 'jobMessageId': 'new-1'}"] = "new-1",
["{'jobMessageId': 'new-2', 'streamId': 'legacy'}"] = "new-2",
["{'jobMessageId': 'new-3'}"] = "new-3"
};
foreach (var (source, expected) in variations)
{
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(DoubleQuotify(source)));
stream.Position = 0;
var recoveredRecord = serializer.ReadObject(stream) as AcquireJobRequest;
Assert.NotNull(recoveredRecord);
Assert.Equal(expected, recoveredRecord.JobMessageId);
}
}
private static string DoubleQuotify(string text)
{
return text.Replace('\'', '"');
}
}

View File

@@ -1,70 +0,0 @@
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);
}
}

View File

@@ -1,4 +1,4 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Common.Util;
using System;
using System.Collections.Concurrent;
using System.Globalization;
@@ -56,9 +56,12 @@ namespace GitHub.Runner.Common.Tests
}
var traceListener = new HostTraceListener(TraceFileName);
_secretMasker = new SecretMasker();
_secretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
_secretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
var encoders = new List<ValueEncoder>()
{
ValueEncoders.JsonStringEscape,
ValueEncoders.UriDataEscape
};
_secretMasker = new SecretMasker(encoders);
_traceManager = new TraceManager(traceListener, null, _secretMasker);
_trace = GetTrace(nameof(TestHostContext));

View File

@@ -1,4 +1,4 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System;
using System.IO;
@@ -89,128 +89,5 @@ namespace GitHub.Runner.Common.Tests.Util
Assert.Equal(gitPath, gitPath2);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void WhichHandlesSymlinkToTargetFullPath()
{
// Arrange
using TestHostContext hc = new TestHostContext(this);
Tracing trace = hc.GetTrace();
string oldValue = Environment.GetEnvironmentVariable(PathUtil.PathVariable);
#if OS_WINDOWS
string newValue = oldValue + @$";{Path.GetTempPath()}";
string symlinkName = $"symlink-{Guid.NewGuid()}";
string symlink = Path.GetTempPath() + $"{symlinkName}.exe";
string target = Path.GetTempPath() + $"target-{Guid.NewGuid()}.exe";
#else
string newValue = oldValue + @$":{Path.GetTempPath()}";
string symlinkName = $"symlink-{Guid.NewGuid()}";
string symlink = Path.GetTempPath() + $"{symlinkName}";
string target = Path.GetTempPath() + $"target-{Guid.NewGuid()}";
#endif
Environment.SetEnvironmentVariable(PathUtil.PathVariable, newValue);
using (File.Create(target))
{
File.CreateSymbolicLink(symlink, target);
// Act.
var result = WhichUtil.Which(symlinkName, require: true, trace: trace);
// Assert
Assert.True(!string.IsNullOrEmpty(result) && File.Exists(result), $"Unable to find symlink through: {nameof(WhichUtil.Which)}");
}
// Cleanup
File.Delete(symlink);
File.Delete(target);
Environment.SetEnvironmentVariable(PathUtil.PathVariable, oldValue);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void WhichHandlesSymlinkToTargetRelativePath()
{
// Arrange
using TestHostContext hc = new TestHostContext(this);
Tracing trace = hc.GetTrace();
string oldValue = Environment.GetEnvironmentVariable(PathUtil.PathVariable);
#if OS_WINDOWS
string newValue = oldValue + @$";{Path.GetTempPath()}";
string symlinkName = $"symlink-{Guid.NewGuid()}";
string symlink = Path.GetTempPath() + $"{symlinkName}.exe";
string targetName = $"target-{Guid.NewGuid()}.exe";
string target = Path.GetTempPath() + targetName;
#else
string newValue = oldValue + @$":{Path.GetTempPath()}";
string symlinkName = $"symlink-{Guid.NewGuid()}";
string symlink = Path.GetTempPath() + $"{symlinkName}";
string targetName = $"target-{Guid.NewGuid()}";
string target = Path.GetTempPath() + targetName;
#endif
Environment.SetEnvironmentVariable(PathUtil.PathVariable, newValue);
using (File.Create(target))
{
File.CreateSymbolicLink(symlink, targetName);
// Act.
var result = WhichUtil.Which(symlinkName, require: true, trace: trace);
// Assert
Assert.True(!string.IsNullOrEmpty(result) && File.Exists(result), $"Unable to find {symlinkName} through: {nameof(WhichUtil.Which)}");
}
// Cleanup
File.Delete(symlink);
File.Delete(target);
Environment.SetEnvironmentVariable(PathUtil.PathVariable, oldValue);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void WhichThrowsWhenSymlinkBroken()
{
// Arrange
using TestHostContext hc = new TestHostContext(this);
Tracing trace = hc.GetTrace();
string oldValue = Environment.GetEnvironmentVariable(PathUtil.PathVariable);
#if OS_WINDOWS
string newValue = oldValue + @$";{Path.GetTempPath()}";
string brokenSymlinkName = $"broken-symlink-{Guid.NewGuid()}";
string brokenSymlink = Path.GetTempPath() + $"{brokenSymlinkName}.exe";
#else
string newValue = oldValue + @$":{Path.GetTempPath()}";
string brokenSymlinkName = $"broken-symlink-{Guid.NewGuid()}";
string brokenSymlink = Path.GetTempPath() + $"{brokenSymlinkName}";
#endif
string target = "no-such-file-cf7e351f";
Environment.SetEnvironmentVariable(PathUtil.PathVariable, newValue);
File.CreateSymbolicLink(brokenSymlink, target);
// Act.
var exception = Assert.Throws<FileNotFoundException>(()=>WhichUtil.Which(brokenSymlinkName, require: true, trace: trace));
// Assert
Assert.Equal(brokenSymlinkName, exception.FileName);
// Cleanup
File.Delete(brokenSymlink);
Environment.SetEnvironmentVariable(PathUtil.PathVariable, oldValue);
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
@@ -32,10 +32,10 @@ namespace GitHub.Runner.Common.Tests.Worker
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((Issue issue, ExecutionContextLogOptions logOptions) =>
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
{
hc.GetTrace().Info($"{issue.Type} {issue.Message} {logOptions.LogMessageOverride ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
});
_commandManager.EnablePluginInternalCommand();
@@ -59,10 +59,10 @@ namespace GitHub.Runner.Common.Tests.Worker
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((Issue issue, ExecutionContextLogOptions logOptions) =>
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
{
hc.GetTrace().Info($"{issue.Type} {issue.Message} {logOptions.LogMessageOverride ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
});
_commandManager.EnablePluginInternalCommand();
@@ -92,10 +92,10 @@ namespace GitHub.Runner.Common.Tests.Worker
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((Issue issue, ExecutionContextLogOptions logOptions) =>
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
{
hc.GetTrace().Info($"{issue.Type} {issue.Message} {logOptions.LogMessageOverride ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
});
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();

View File

@@ -29,7 +29,6 @@ namespace GitHub.Runner.Common.Tests.Worker
private Mock<IDockerCommandManager> _dockerManager;
private Mock<IExecutionContext> _ec;
private Mock<IJobServer> _jobServer;
private Mock<ILaunchServer> _launchServer;
private Mock<IRunnerPluginManager> _pluginManager;
private TestHostContext _hc;
private ActionManager _actionManager;
@@ -2148,7 +2147,7 @@ runs:
_ec.Object.Global.FileTable = new List<String>();
_ec.Object.Global.Plan = new TaskOrchestrationPlanReference();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"[{tag}]{message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>())).Callback((Issue issue, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); });
_ec.Setup(x => x.GetGitHubContext("workspace")).Returns(Path.Combine(_workFolder, "actions", "actions"));
_dockerManager = new Mock<IDockerCommandManager>();
@@ -2176,25 +2175,6 @@ runs:
return Task.FromResult(result);
});
_launchServer = new Mock<ILaunchServer>();
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>()))
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)
{
var key = $"{action.NameWithOwner}@{action.Ref}";
result.Actions[key] = new ActionDownloadInfo
{
NameWithOwner = action.NameWithOwner,
Ref = action.Ref,
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
};
}
return Task.FromResult(result);
});
_pluginManager = new Mock<IRunnerPluginManager>();
_pluginManager.Setup(x => x.GetPluginAction(It.IsAny<string>())).Returns(new RunnerPluginActionInfo() { PluginTypeName = "plugin.class, plugin", PostPluginTypeName = "plugin.cleanup, plugin" });
@@ -2203,7 +2183,6 @@ runs:
_hc.SetSingleton<IDockerCommandManager>(_dockerManager.Object);
_hc.SetSingleton<IJobServer>(_jobServer.Object);
_hc.SetSingleton<ILaunchServer>(_launchServer.Object);
_hc.SetSingleton<IRunnerPluginManager>(_pluginManager.Object);
_hc.SetSingleton<IActionManifestManager>(actionManifest);
_hc.SetSingleton<IHttpClientHandlerFactory>(new HttpClientHandlerFactory());

View File

@@ -670,7 +670,7 @@ namespace GitHub.Runner.Common.Tests.Worker
{
Teardown();
}
}
}
[Fact]
[Trait("Level", "L0")]
@@ -715,7 +715,7 @@ namespace GitHub.Runner.Common.Tests.Worker
//Assert
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
Assert.Contains($"Fail to load {action_path}", err.Message);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12' or 'node16'.")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12' or 'node16'.")), It.IsAny<string>()), Times.Once);
}
finally
{
@@ -860,7 +860,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());
_ec.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>())).Callback((Issue issue, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); });
}
private void Teardown()

View File

@@ -1,4 +1,4 @@
using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines.ContextData;
@@ -366,7 +366,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("invalid1", finialInputs["invalid1"]);
Assert.Equal("invalid2", finialInputs["invalid2"]);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Unexpected input(s) 'invalid1', 'invalid2'")), It.IsAny<ExecutionContextLogOptions>()), Times.Once);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Unexpected input(s) 'invalid1', 'invalid2'")), It.IsAny<string>()), Times.Once);
}
[Fact]
@@ -485,7 +485,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
_ec.Object.Global.Variables = new Variables(_hc, new Dictionary<string, VariableValue>());
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"[{tag}]{message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>())).Callback((Issue issue, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); });
_hc.SetSingleton<IActionManager>(_actionManager.Object);
_hc.SetSingleton<IHandlerFactory>(_handlerFactory.Object);

View File

@@ -247,16 +247,12 @@ namespace GitHub.Runner.Common.Tests.Worker
WriteDebug = true,
Variables = _variables,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
@@ -272,4 +268,4 @@ namespace GitHub.Runner.Common.Tests.Worker
return hostContext;
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -52,43 +52,42 @@ namespace GitHub.Runner.Common.Tests.Worker
// Act.
ec.InitializeJob(jobRequest, CancellationToken.None);
// Flood the ExecutionContext with errors and warnings (past its max capacity of 10).
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.Complete();
// Assert.
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.ErrorCount > 0)), Times.AtLeast(10));
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.WarningCount > 0)), Times.AtLeast(10));
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.ErrorCount == 15)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.WarningCount == 14)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Type == IssueType.Error).Count() == 10)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Type == IssueType.Warning).Count() == 10)), Times.AtLeastOnce);
}
@@ -191,9 +190,9 @@ namespace GitHub.Runner.Common.Tests.Worker
bigMessage += "a";
}
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = bigMessage }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = bigMessage }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = bigMessage }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = bigMessage });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = bigMessage });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = bigMessage });
ec.Complete();
@@ -204,61 +203,6 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void AddIssue_OverrideLogMessage()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Create a job request message.
TaskOrchestrationPlanReference plan = new();
TimelineReference timeline = new();
Guid jobId = Guid.NewGuid();
string jobName = "some job name";
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
{
Alias = Pipelines.PipelineConstants.SelfAlias,
Id = "github",
Version = "sha1"
});
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
// Arrange: Setup the paging logger.
var pagingLogger = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger.Object);
hc.SetSingleton(jobServerQueue.Object);
var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
// Act.
ec.InitializeJob(jobRequest, CancellationToken.None);
var issueMessage = "Message embedded in issue.";
var overrideMessage = "Message override.";
var options = new ExecutionContextLogOptions(true, overrideMessage);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = issueMessage }, options);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = issueMessage }, options);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = issueMessage }, options);
// Finally, add a variation that DOESN'T override the message.
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = issueMessage }, ExecutionContextLogOptions.Default);
ec.Complete();
// Assert.
jobServerQueue.Verify(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<long?>()), Times.Exactly(4));
jobServerQueue.Verify(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(text => text.EndsWith(overrideMessage)), It.IsAny<long?>()), Times.Exactly(3));
jobServerQueue.Verify(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(text => text.EndsWith(issueMessage)), It.IsAny<long?>()), Times.Exactly(1));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -298,15 +242,13 @@ namespace GitHub.Runner.Common.Tests.Worker
var embeddedStep = ec.CreateChild(Guid.NewGuid(), "action_1_pre", "action_1_pre", null, null, ActionRunStage.Main, isEmbedded: true);
embeddedStep.Start();
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error annotation that should have step and line number information" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning annotation that should have step and line number information" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice annotation that should have step and line number information" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error annotation that should have step and line number information" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning annotation that should have step and line number information" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice annotation that should have step and line number information" });
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()), Times.AtLeastOnce);
// Verify that Error/Warning/Notice issues added to embedded steps don't get sent up to the server.
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Error).Count() == 1)), Times.Never);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Warning).Count() == 1)), Times.Never);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Notice).Count() == 1)), Times.Never);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Error).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Warning).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Notice).Count() == 1)), Times.AtLeastOnce);
}
}
@@ -684,12 +626,12 @@ namespace GitHub.Runner.Common.Tests.Worker
ec.StepTelemetry.StepId = Guid.NewGuid();
ec.StepTelemetry.Stage = "main";
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
ec.Complete();
@@ -750,9 +692,9 @@ namespace GitHub.Runner.Common.Tests.Worker
embeddedStep.StepTelemetry.Action = "actions/checkout";
embeddedStep.StepTelemetry.Ref = "v2";
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
embeddedStep.PublishStepTelemetry();
@@ -814,9 +756,9 @@ namespace GitHub.Runner.Common.Tests.Worker
embeddedStep.StepTelemetry.Action = "actions/checkout";
embeddedStep.StepTelemetry.Ref = "v2";
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
ec.Complete();
@@ -985,7 +927,7 @@ namespace GitHub.Runner.Common.Tests.Worker
inputVarsContext["VARIABLE_2"] = new StringContextData("value2");
jobRequest.ContextData["vars"] = inputVarsContext;
// Arrange: Setup the paging logger.
// Arrange: Setup the paging logger.
var pagingLogger1 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger1.Object);
@@ -999,7 +941,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var expected = new DictionaryContextData();
expected["VARIABLE_1"] = new StringContextData("value1");
expected["VARIABLE_2"] = new StringContextData("value1");
Assert.True(ExpressionValuesAssertEqual(expected, jobContext.ExpressionValues["vars"] as DictionaryContextData));
}
}
@@ -1030,7 +972,7 @@ namespace GitHub.Runner.Common.Tests.Worker
inputVarsContext[Constants.Variables.Actions.RunnerDebug] = new StringContextData("true");
jobRequest.ContextData["vars"] = inputVarsContext;
// Arrange: Setup the paging logger.
// Arrange: Setup the paging logger.
var pagingLogger1 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger1.Object);
@@ -1041,7 +983,7 @@ namespace GitHub.Runner.Common.Tests.Worker
jobContext.InitializeJob(jobRequest, CancellationToken.None);
Assert.Equal("true", jobContext.Global.Variables.Get(Constants.Variables.Actions.StepDebug));
Assert.Equal("true", jobContext.Global.Variables.Get(Constants.Variables.Actions.RunnerDebug));
}
@@ -1076,7 +1018,7 @@ namespace GitHub.Runner.Common.Tests.Worker
jobRequest.Variables[Constants.Variables.Actions.StepDebug] = "false";
jobRequest.Variables[Constants.Variables.Actions.RunnerDebug] = "false";
// Arrange: Setup the paging logger.
// Arrange: Setup the paging logger.
var pagingLogger1 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger1.Object);
@@ -1087,7 +1029,7 @@ namespace GitHub.Runner.Common.Tests.Worker
jobContext.InitializeJob(jobRequest, CancellationToken.None);
Assert.Equal("false", jobContext.Global.Variables.Get(Constants.Variables.Actions.StepDebug));
Assert.Equal("false", jobContext.Global.Variables.Get(Constants.Variables.Actions.RunnerDebug));
}
@@ -1119,4 +1061,4 @@ namespace GitHub.Runner.Common.Tests.Worker
return true;
}
}
}
}

View File

@@ -984,15 +984,10 @@ namespace GitHub.Runner.Common.Tests.Worker
{
_onMatcherChanged = handler;
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>

View File

@@ -413,16 +413,12 @@ namespace GitHub.Runner.Common.Tests.Worker
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>

View File

@@ -411,16 +411,12 @@ namespace GitHub.Runner.Common.Tests.Worker
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>

View File

@@ -413,16 +413,12 @@ namespace GitHub.Runner.Common.Tests.Worker
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
@@ -434,8 +430,8 @@ namespace GitHub.Runner.Common.Tests.Worker
_executionContext.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference))
.Callback((string name, string value, out string reference) =>
{
reference = value;
_outputs[name] = value;
reference = value;
_outputs[name] = value;
});
// SetOutputFileCommand

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -337,7 +337,7 @@ namespace GitHub.Runner.Common.Tests.Worker
// Act.
await _stepsRunner.RunAsync(jobContext: _ec.Object);
// Assert.
// Assert.
Assert.Equal(2, variableSet.Step.Length);
variableSet.Step[0].Verify(x => x.RunAsync());
variableSet.Step[1].Verify(x => x.RunAsync(), variableSet.Expected ? Times.Once() : Times.Never());
@@ -590,7 +590,7 @@ namespace GitHub.Runner.Common.Tests.Worker
step.Setup(x => x.Condition).Returns(condition);
step.Setup(x => x.ContinueOnError).Returns(new BooleanToken(null, null, null, continueOnError));
step.Setup(x => x.Action)
.Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
.Returns(new DistributedTask.Pipelines.ActionStep()
{
Name = name,
Id = Guid.NewGuid(),

View File

@@ -1,70 +0,0 @@
minimal: |
{ "Name": "minimal" }
invalid-attempt-value: |
{
"Name": "invalid-attempt-value",
"Attempt": -99
}
zero-attempt-value: |
{
"Name": "zero-attempt-value",
"Attempt": 0
}
legacy-nulls: |
{
"Name": "legacy-nulls",
"ErrorCount": null,
"WarningCount": null,
"NoticeCount": null
}
missing-counts: |
{
"Name": "missing-counts"
}
non-zero-counts: |
{
"Name": "non-zero-counts",
"ErrorCount": 10,
"WarningCount": 20,
"NoticeCount": 30
}
explicit-null-collections: |
{
"Name": "explicit-null-collections",
"Issues": null,
"PreviousAttempts": null,
"Variables": null
}
lean: |
{
"Id": "00000000-0000-0000-0000-000000000000",
"Name": "lean",
"LastModified": "\/Date(1679073003252+0000)\/",
"Issues": [
{
"Type": 0,
"Category": null,
"Message": null,
"IsInfrastructureIssue": null
}
],
"Variables": [
{ "Key": "x", "Value": { "Value": "1" } },
{ "Key": "y", "Value": { "Value": "2" } },
{ "Key": "z", "Value": { "Value": "3" } }
],
"Attempt": 4,
"PreviousAttempts": [
{ "Attempt": 1 },
{ "Attempt": 2 },
{ "Attempt": 3 }
]
}
duplicate-variable-keys: |
{
"Name": "duplicate-variable-keys",
"Variables": [
{ "Key": "aaa", "Value": { "Value": "a.1" } },
{ "Key": "AAA", "Value": { "Value": "a.2" } }
]
}

View File

@@ -1 +1 @@
2.304.0
2.303.0