mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
4 Commits
v2.304.1
...
users/jww3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afcca9bfa4 | ||
|
|
157e03616e | ||
|
|
30f686b9c2 | ||
|
|
ec5d72810f |
@@ -1,19 +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)
|
||||
- Add warning to notify about forcing actions to run on node16 instead of node12 (#2678)
|
||||
- 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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.304.1
|
||||
<Update to ./src/runnerversion when creating release>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
@@ -170,8 +170,6 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string UnsupportedSummarySize = "$GITHUB_STEP_SUMMARY upload aborted, supports content up to a size of {0}k, got {1}k. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
|
||||
public static readonly string SummaryUploadError = "$GITHUB_STEP_SUMMARY upload aborted, an error occurred when uploading the summary. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
|
||||
public static readonly string Node12DetectedAfterEndOfLife = "Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: {0}. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.";
|
||||
public static readonly string EnforcedNode12DetectedAfterEndOfLife = "The following actions uses node12 which is deprecated and will be forced to run on node16: {0}. For more info: https://github.blog/changelog/2023-06-13-github-actions-all-actions-will-run-on-node16-instead-of-node12-by-default/";
|
||||
public static readonly string EnforcedNode12DetectedAfterEndOfLifeEnvVariable = "Node16ForceActionsWarnings";
|
||||
}
|
||||
|
||||
public static class RunnerEvent
|
||||
@@ -263,7 +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 OrchestrationId = "system.orchestrationId";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 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.");
|
||||
}
|
||||
|
||||
@@ -387,16 +371,9 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
@@ -643,12 +620,8 @@ namespace GitHub.Runner.Common
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!_resultsServiceOnly)
|
||||
{
|
||||
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_resultsClientInitiated)
|
||||
@@ -823,8 +796,6 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
bool uploadSucceed = false;
|
||||
try
|
||||
{
|
||||
if (!_resultsServiceOnly)
|
||||
{
|
||||
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -849,7 +820,6 @@ namespace GitHub.Runner.Common
|
||||
var result = await _jobServer.CreateAttachmentAsync(_scopeIdentifier, _hubName, _planId, file.TimelineId, file.TimelineRecordId, file.Type, file.Name, fs, default(CancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadSucceed = true;
|
||||
}
|
||||
|
||||
@@ -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.Error(ex);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,14 @@ namespace GitHub.Runner.Common
|
||||
public Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection();
|
||||
return RetryRequest<AgentJobRequestMessage>(
|
||||
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, CancellationToken cancellationToken)
|
||||
@@ -65,8 +71,14 @@ namespace GitHub.Runner.Common
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
namespace GitHub.Runner.Sdk
|
||||
{
|
||||
@@ -15,7 +15,6 @@ namespace GitHub.Runner.Sdk
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,28 +68,6 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
bool isOptOut = isWorkflowOptOutSet ? StringUtil.ConvertToBoolean(workflowOptOut) : isLocalOptOut;
|
||||
if (!isOptOut)
|
||||
{
|
||||
var repoAction = action as Pipelines.RepositoryPathReference;
|
||||
if (repoAction != null)
|
||||
{
|
||||
var warningActions = new HashSet<string>();
|
||||
if (executionContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode12DetectedAfterEndOfLifeEnvVariable, out var node16ForceWarnings))
|
||||
{
|
||||
warningActions = StringUtil.ConvertFromJson<HashSet<string>>(node16ForceWarnings);
|
||||
}
|
||||
|
||||
var repoActionFullName = "";
|
||||
if (string.IsNullOrEmpty(repoAction.Name))
|
||||
{
|
||||
repoActionFullName = repoAction.Path; // local actions don't have a 'Name'
|
||||
}
|
||||
else
|
||||
{
|
||||
repoActionFullName = $"{repoAction.Name}/{repoAction.Path ?? string.Empty}".TrimEnd('/') + $"@{repoAction.Ref}";
|
||||
}
|
||||
|
||||
warningActions.Add(repoActionFullName);
|
||||
executionContext.Global.Variables.Set("Node16ForceActionsWarnings", StringUtil.ConvertToJson(warningActions));
|
||||
}
|
||||
nodeData.NodeVersion = "node16";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +49,6 @@ namespace GitHub.Runner.Worker
|
||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
||||
server = runServer;
|
||||
|
||||
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
|
||||
_jobServerQueue.Start(message, resultServiceOnly: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -348,12 +337,6 @@ namespace GitHub.Runner.Worker
|
||||
jobContext.Warning(string.Format(Constants.Runner.Node12DetectedAfterEndOfLife, actions));
|
||||
}
|
||||
|
||||
if (jobContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode12DetectedAfterEndOfLifeEnvVariable, out var node16ForceWarnings))
|
||||
{
|
||||
var actions = string.Join(", ", StringUtil.ConvertFromJson<HashSet<string>>(node16ForceWarnings));
|
||||
jobContext.Warning(string.Format(Constants.Runner.EnforcedNode12DetectedAfterEndOfLife, actions));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ShutdownQueue(throwOnFailure: true);
|
||||
|
||||
@@ -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()}");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace GitHub.DistributedTask.Logging
|
||||
@@ -8,7 +8,6 @@ namespace GitHub.DistributedTask.Logging
|
||||
{
|
||||
void AddRegex(String pattern);
|
||||
void AddValue(String value);
|
||||
void AddValueEncoder(ValueEncoder encoder);
|
||||
ISecretMasker Clone();
|
||||
String MaskSecrets(String input);
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -104,15 +104,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 +131,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()
|
||||
|
||||
@@ -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)
|
||||
// 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++)
|
||||
{
|
||||
return Base64StringEscapeShift(value, 2);
|
||||
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,10 +102,10 @@ 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.
|
||||
@@ -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 = '=';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +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; }
|
||||
|
||||
// This field will be removed in an upcoming Runner release.
|
||||
// It's left here temporarily to facilitate the transition to the new field name, JobMessageId.
|
||||
[DataMember(Name = "streamId", EmitDefaultValue = false)]
|
||||
public string StreamId { get; set; }
|
||||
public string StreamID { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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,34 +63,20 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
HttpMethod httpMethod = new HttpMethod("POST");
|
||||
var payload = new AcquireJobRequest
|
||||
{
|
||||
JobMessageId = messageId,
|
||||
StreamId = 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}");
|
||||
default:
|
||||
throw new Exception($"Failed to get job message: {result.Error}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CompleteJobAsync(
|
||||
public Task CompleteJobAsync(
|
||||
Uri requestUri,
|
||||
Guid planId,
|
||||
Guid jobId,
|
||||
@@ -113,26 +98,14 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
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,
|
||||
@@ -148,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<word>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")]
|
||||
|
||||
@@ -1,63 +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 jobMessageId = "1526919030369-33";
|
||||
var request = new AcquireJobRequest
|
||||
{
|
||||
JobMessageId = jobMessageId,
|
||||
StreamId = jobMessageId
|
||||
};
|
||||
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}','streamId':'{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('\'', '"');
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.304.1
|
||||
2.303.0
|
||||
|
||||
Reference in New Issue
Block a user