Compare commits

..

15 Commits

Author SHA1 Message Date
Tatyana Kostromskaya
6bde7fe413 Update release version 2023-07-11 11:27:57 +00:00
Tatyana Kostromskaya
2494aa2b0d Add warning to notify about forcing node16 (#2678)
* add warning

* fix tests

* aggregate to one warning

* fix linter

* fix comm
2023-07-11 11:13:53 +00:00
Tingluo Huang
9ef4121b5b Create 2.304.0 runner release 2023-04-26 16:04:26 -04:00
Tingluo Huang
58f3ff55aa Prepare 2.304.0 runner release. (#2569) 2023-04-26 15:48:13 -04:00
Tingluo Huang
6353ac84d7 Add orchestrationId to useragent for better correlation. (#2568)
* Add orchestrationId to useragent for better correlation.

* .
2023-04-26 19:15:19 +00:00
Yang Cao
ad9a4a45d1 Bypass job server when run service is enabled (#2516)
* Only upload to Results with new job message type

* No need to have separate websocketFeedServer

* Linting fix

* Update src/Runner.Common/JobServerQueue.cs

Co-authored-by: Tingluo Huang <tingluohuang@github.com>

* add connection timeout

* Consolidate initializing webclient to result client

* Retry websocket delivery for console logs

* Linter fix

* Do not give up for good, reconnect again in 10 minutes

* Has to reset delivered

* Only first time retry 3 times to connect to websocket

---------

Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2023-04-26 12:51:56 -04:00
John Wesley Walker III
a41397ae93 Rename AcquireJobRequest::StreamID to AcquireJobRequest::JobMessageID (#2547)
* Rename AcquireJobRequest::StreamID to AcquireJobRequest::JobMessageID to match corresponding server-side update.
* appeased the linter
* Added unit tests to prove AcquireJobRequest serialization/deserialization is as-expected.
* Distinguish unit test variations.
* Incorporated PR Feedback.
2023-04-26 18:31:41 +02:00
Erez Testiler
c4d41e95cb Add *.ghe.localhost domains to hosted server check (#2536) 2023-04-24 12:52:28 -04:00
Maggie Spletzer
af6ed41bcb Adding curl --retry-all-errors for external tool downloads (#2557)
* adding retry-all-errors based on curl version >= 7.71.0

* adding logging

* fixing conditional logic
2023-04-20 21:01:11 +00:00
Tingluo Huang
e8b2380a20 Limit the time we wait for waiting websocket to connect. (#2554) 2023-04-19 10:20:00 -04:00
Maggie Spletzer
38ab9dedf4 Adding curl retry for external tool downloads (#2552)
* adding retry for flaky downloads

* adding comment

* Update src/Misc/externals.sh

Co-authored-by: Tingluo Huang <tingluohuang@github.com>

---------

Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2023-04-18 18:31:46 +00:00
Yashwanth Anantharaju
c7629700ad Handle non success raw http responses (#2541)
* handle non success responses

* fix format

* nits

Co-authored-by: Tingluo Huang <tingluohuang@github.com>

---------

Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2023-04-14 16:17:20 -04:00
Yang Cao
b9a0b5dba9 If conclusion is not set, we cannot get default value (#2535) 2023-04-13 13:03:57 -04:00
Yang Cao
766cefe599 Also send in conclusion for steps (#2531) 2023-04-13 15:19:00 +02:00
Eli Entelis
2ecd7d2fc6 which handles broken symlink & unit test added (#2150) (#2196)
* which handles broken symlink & unit test added (#2150)

* Update src/Runner.Sdk/Util/WhichUtil.cs

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>

* fix pr comments

* trace log added,  in case we find a symlink that is broken

* add check in case the target of the symlink is a relative path; added test that check symlink that targets a full path; added test that check symlink that targets a relative path;

* fix tests

* fix tests for linux

---------

Co-authored-by: Eli Entelis <eli.entelis@hexagon.com>
Co-authored-by: Eli Entelis <42981948+Eliminator1999@users.noreply.github.com>
Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>
2023-04-06 22:15:11 -04:00
31 changed files with 812 additions and 321 deletions

View File

@@ -1,17 +1,19 @@
## Features
- 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)
- 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)
## Bugs
- Exit on runner version deprecation error (#2299)
- Runner service exit after consecutive re-try exits (#2426)
- 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)
## Misc
- 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)
- Bump container hooks version to 0.3.1 in runner image (#2496)
- Runner changes to communicate with vNext services (#2487, #2500, #2505, #2541, #2547)
_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

@@ -1 +1 @@
<Update to ./src/runnerversion when creating release>
2.304.1

View File

@@ -55,12 +55,23 @@ 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
curl -fkSL -o "$partial_target" "$download_source" 2>"${download_target}_download.log" || checkRC 'curl'
# --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
# 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
{
@@ -170,6 +170,8 @@ 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
@@ -261,6 +263,7 @@ 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";
}
}

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;
private readonly ISecretMasker _secretMasker = new 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,20 +82,17 @@ namespace GitHub.Runner.Common
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
_loadContext.Unloading += LoadContext_Unloading;
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);
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);
// Create StdoutTraceListener if ENV is set
StdoutTraceListener stdoutTraceListener = null;

View File

@@ -199,13 +199,15 @@ namespace GitHub.Runner.Common
{
Trace.Info($"Attempting to start websocket client with delay {delay}.");
await Task.Delay(delay);
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), default(CancellationToken));
using var connectTimeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), connectTimeoutTokenSource.Token);
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;
}
}

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);
void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultServiceOnly = false);
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,6 +70,7 @@ 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;
@@ -95,13 +96,17 @@ namespace GitHub.Runner.Common
_resultsServer = hostContext.GetService<IResultsServer>();
}
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
public void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultServiceOnly = false)
{
Trace.Entering();
_resultsServiceOnly = resultServiceOnly;
var serviceEndPoint = jobRequest.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
_jobServer.InitializeWebsocketClient(serviceEndPoint);
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);
@@ -112,8 +117,16 @@ namespace GitHub.Runner.Common
!string.IsNullOrEmpty(accessToken) &&
!string.IsNullOrEmpty(resultsReceiverEndpoint))
{
string liveConsoleFeedUrl = null;
Trace.Info("Initializing results client");
_resultsServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), accessToken);
if (resultServiceOnly
&& serviceEndPoint.Data.TryGetValue("FeedStreamUrl", out var feedStreamUrl)
&& !string.IsNullOrEmpty(feedStreamUrl))
{
liveConsoleFeedUrl = feedStreamUrl;
}
_resultsServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), liveConsoleFeedUrl, accessToken);
_resultsClientInitiated = true;
}
@@ -194,6 +207,9 @@ 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.");
}
@@ -372,7 +388,14 @@ namespace GitHub.Runner.Common
// Give at most 60s for each request.
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
{
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), batch[0].LineNumber, timeoutTokenSource.Token);
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)
@@ -599,7 +622,7 @@ namespace GitHub.Runner.Common
foreach (var detailTimeline in update.PendingRecords.Where(r => r.Details != null))
{
if (!_allTimelines.Contains(detailTimeline.Details.Id))
if (!_resultsServiceOnly && !_allTimelines.Contains(detailTimeline.Details.Id))
{
try
{
@@ -621,7 +644,11 @@ namespace GitHub.Runner.Common
try
{
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
if (!_resultsServiceOnly)
{
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
}
try
{
if (_resultsClientInitiated)
@@ -797,27 +824,30 @@ namespace GitHub.Runner.Common
bool uploadSucceed = false;
try
{
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
if (!_resultsServiceOnly)
{
// 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))
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
{
var logUploaded = await _jobServer.AppendLogContentAsync(_scopeIdentifier, _hubName, _planId, taskLog.Id, fs, default(CancellationToken));
// 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);
}
// 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))
else
{
var result = await _jobServer.CreateAttachmentAsync(_scopeIdentifier, _hubName, _planId, file.TimelineId, file.TimelineRecordId, file.Type, file.Name, fs, default(CancellationToken));
// 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));
}
}
}

View File

@@ -1,17 +1,26 @@
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
public interface IResultsServer : IRunnerService, IAsyncDisposable
{
void InitializeResultsClient(Uri uri, string token);
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);
// logging and console
Task CreateResultsStepSummaryAsync(string planId, string jobId, Guid stepId, string file,
@@ -31,10 +40,26 @@ namespace GitHub.Runner.Common
{
private ResultsHttpClient _resultsClient;
public void InitializeResultsClient(Uri uri, string token)
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)
{
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,
@@ -94,5 +119,144 @@ 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}");
}
}
}
}

View File

@@ -51,14 +51,8 @@ namespace GitHub.Runner.Common
public Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken cancellationToken)
{
CheckConnection();
var jobMessage = RetryRequest<AgentJobRequestMessage>(
return 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)
@@ -71,14 +65,8 @@ namespace GitHub.Runner.Common
public Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken cancellationToken)
{
CheckConnection();
var renewJobResponse = RetryRequest<RenewJobResponse>(
return 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;
namespace GitHub.Runner.Sdk
{
@@ -11,10 +11,11 @@ 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)
if (matches != null && matches.Length > 0 && IsPathValid(matches.First(), trace))
{
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)))
if (matches.Any(p => p.Equals(fullPath, StringComparison.OrdinalIgnoreCase)) && IsPathValid(fullPath, trace))
{
trace?.Info($"Location: '{fullPath}'");
return fullPath;
@@ -105,7 +105,7 @@ namespace GitHub.Runner.Sdk
trace?.Verbose(ex.ToString());
}
if (matches != null && matches.Length > 0)
if (matches != null && matches.Length > 0 && IsPathValid(matches.First(), trace))
{
trace?.Info($"Location: '{matches.First()}'");
return matches.First();
@@ -128,5 +128,15 @@ 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

@@ -1,4 +1,4 @@
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker.Container;
using System;
@@ -405,9 +405,8 @@ 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 auxiliarySecrets = command.Data.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(candidate => candidate.Length >= HostContext.SecretMasker.DerivedSecretRecommendedMinimumLength);
foreach (var item in auxiliarySecrets)
var split = command.Data.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var item in split)
{
HostContext.SecretMasker.AddValue(item);
}

View File

@@ -68,6 +68,28 @@ 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";
}
}

View File

@@ -3,6 +3,7 @@ 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;
@@ -42,6 +43,13 @@ 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))
{
@@ -49,6 +57,9 @@ 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
{
@@ -337,6 +348,12 @@ 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);

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,9 +156,8 @@ namespace GitHub.Runner.Worker
HostContext.SecretMasker.AddValue(value);
// Also add each individual line. Typically individual lines are processed from STDOUT of child processes.
var auxiliarySecrets = value.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(candidate => candidate.Length >= HostContext.SecretMasker.DerivedSecretRecommendedMinimumLength);
foreach (var item in auxiliarySecrets)
var split = value.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var item in split)
{
HostContext.SecretMasker.AddValue(item);
}

View File

@@ -1,6 +1,6 @@
using System;
using System;
using System.Collections.Generic;
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(Enumerable.Empty<ValueEncoder>());
secretMasker = secretMasker?.Clone() ?? new SecretMasker();
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(IEnumerable<ValueEncoder> encoders)
public SecretMasker()
{
m_originalValueSecrets = new HashSet<ValueSecret>();
m_regexSecrets = new HashSet<RegexSecret>();
m_valueEncoders = new HashSet<ValueEncoder>(encoders ?? Enumerable.Empty<ValueEncoder>());
m_valueEncoders = new HashSet<ValueEncoder>();
m_valueSecrets = new HashSet<ValueSecret>();
}
@@ -40,19 +40,6 @@ 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>
@@ -117,11 +104,15 @@ namespace GitHub.DistributedTask.Logging
}
}
var secretVariations = valueEncoders.SelectMany(encoder => encoder(value))
.Where(variation => !string.IsNullOrEmpty(variation))
.Distinct()
.Select(variation => new ValueSecret(variation));
valueSecrets.AddRange(secretVariations);
// Compute the encoded values.
foreach (ValueEncoder valueEncoder in valueEncoders)
{
String encodedValue = valueEncoder(value);
if (!String.IsNullOrEmpty(encodedValue))
{
valueSecrets.Add(new ValueSecret(encodedValue));
}
}
// Write section.
try
@@ -144,6 +135,69 @@ 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,97 +1,73 @@
using System;
using System.Collections.Generic;
using System;
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 IEnumerable<string> ValueEncoder(string value);
public delegate String ValueEncoder(String value);
[EditorBrowsable(EditorBrowsableState.Never)]
public static class ValueEncoders
{
public static IEnumerable<string> EnumerateBase64Variations(string value)
public static String Base64StringEscape(String value)
{
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 |
return Convert.ToBase64String(Encoding.UTF8.GetBytes(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)
// 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)
{
return Base64StringEscapeShift(value, 1);
}
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();
}
}
}
public static String Base64StringEscapeShift2(String value)
{
return Base64StringEscapeShift(value, 2);
}
// Used when we pass environment variables to docker to escape " with \"
public static IEnumerable<string> CommandLineArgumentEscape(string value)
public static String CommandLineArgumentEscape(String value)
{
yield return value.Replace("\"", "\\\"");
return value.Replace("\"", "\\\"");
}
public static IEnumerable<string> ExpressionStringEscape(string value)
public static String ExpressionStringEscape(String value)
{
yield return Expressions2.Sdk.ExpressionUtility.StringEscape(value);
return Expressions2.Sdk.ExpressionUtility.StringEscape(value);
}
public static IEnumerable<string> JsonStringEscape(string value)
public static 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);
yield return jsonEscapedValue;
return jsonEscapedValue;
}
public static IEnumerable<string> UriDataEscape(string value)
public static String UriDataEscape(String value)
{
yield return UriDataEscape(value, 65519);
return UriDataEscape(value, 65519);
}
public static IEnumerable<string> XmlDataEscape(string value)
public static String XmlDataEscape(String value)
{
yield return SecurityElement.Escape(value);
return SecurityElement.Escape(value);
}
public static IEnumerable<string> TrimDoubleQuotes(string value)
public static String TrimDoubleQuotes(String value)
{
var trimmed = string.Empty;
if (!string.IsNullOrEmpty(value) &&
@@ -102,17 +78,17 @@ namespace GitHub.DistributedTask.Logging
trimmed = value.Substring(1, value.Length - 2);
}
yield return trimmed;
return trimmed;
}
public static IEnumerable<string> PowerShellPreAmpersandEscape(string value)
public static 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.
@@ -136,10 +112,10 @@ namespace GitHub.DistributedTask.Logging
}
}
yield return trimmed;
return trimmed;
}
public static IEnumerable<string> PowerShellPostAmpersandEscape(string value)
public static String PowerShellPostAmpersandEscape(String value)
{
var trimmed = string.Empty;
if (!string.IsNullOrEmpty(value) && value.Contains("&"))
@@ -161,10 +137,27 @@ namespace GitHub.DistributedTask.Logging
}
}
yield return trimmed;
return trimmed;
}
private static string UriDataEscape(string value, Int32 maxSegmentSize)
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)
{
if (value.Length <= maxSegmentSize)
{
@@ -190,7 +183,5 @@ namespace GitHub.DistributedTask.Logging
return result.ToString();
}
private const char BASE64_PADDING_SUFFIX = '=';
}
}

View File

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

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -55,7 +56,7 @@ namespace GitHub.Actions.RunService.WebApi
{
}
public Task<AgentJobRequestMessage> GetJobMessageAsync(
public async Task<AgentJobRequestMessage> GetJobMessageAsync(
Uri requestUri,
string messageId,
CancellationToken cancellationToken = default)
@@ -63,20 +64,34 @@ namespace GitHub.Actions.RunService.WebApi
HttpMethod httpMethod = new HttpMethod("POST");
var payload = new AcquireJobRequest
{
StreamID = messageId
JobMessageId = messageId,
StreamId = messageId,
};
requestUri = new Uri(requestUri, "acquirejob");
var requestContent = new ObjectContent<AcquireJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
return SendAsync<AgentJobRequestMessage>(
var result = await 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 Task CompleteJobAsync(
public async Task CompleteJobAsync(
Uri requestUri,
Guid planId,
Guid jobId,
@@ -98,14 +113,26 @@ namespace GitHub.Actions.RunService.WebApi
requestUri = new Uri(requestUri, "completejob");
var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
return SendAsync(
var response = await 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 Task<RenewJobResponse> RenewJobAsync(
public async Task<RenewJobResponse> RenewJobAsync(
Uri requestUri,
Guid planId,
Guid jobId,
@@ -121,11 +148,24 @@ namespace GitHub.Actions.RunService.WebApi
requestUri = new Uri(requestUri, "renewjob");
var requestContent = new ObjectContent<RenewJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
return SendAsync<RenewJobResponse>(
var result = await 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

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -55,7 +56,7 @@ namespace GitHub.Actions.RunService.WebApi
{
}
public Task<TaskAgentMessage> GetRunnerMessageAsync(
public async Task<TaskAgentMessage> GetRunnerMessageAsync(
string runnerVersion,
TaskAgentStatus? status,
CancellationToken cancellationToken = default
@@ -74,11 +75,18 @@ namespace GitHub.Actions.RunService.WebApi
queryParams.Add("runnerVersion", runnerVersion);
}
return SendAsync<TaskAgentMessage>(
var result = await 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,6 +157,8 @@ namespace GitHub.Services.Results.Contracts
public string StartedAt;
[DataMember]
public string CompletedAt;
[DataMember]
public Conclusion Conclusion;
}
public enum Status
@@ -167,6 +169,15 @@ 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

@@ -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<T> SendAsync<T>(
protected Task<RawHttpClientResult<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<T> SendAsync<T>(
protected async Task<RawHttpClientResult<T>> SendAsync<T>(
HttpMethod method,
IEnumerable<KeyValuePair<String, String>> additionalHeaders,
Uri requestUri,
@@ -128,7 +128,7 @@ namespace Sdk.WebApi.WebApi
}
}
protected async Task<T> SendAsync<T>(
protected async Task<RawHttpClientResult<T>> SendAsync<T>(
HttpRequestMessage message,
Object userState = null,
CancellationToken cancellationToken = default(CancellationToken))
@@ -138,7 +138,16 @@ namespace Sdk.WebApi.WebApi
//from deadlocking...
using (HttpResponseMessage response = await this.SendAsync(message, userState, cancellationToken).ConfigureAwait(false))
{
return await ReadContentAsAsync<T>(response, 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);
}
}
}

View File

@@ -0,0 +1,33 @@
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,7 +329,8 @@ 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)
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat),
Conclusion = ConvertResultToConclusion(r.Result)
};
}
@@ -348,6 +349,29 @@ 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,3 +1,4 @@
using GitHub.Runner.Common.Util;
using System;
using System.IO;
using System.Reflection;
@@ -12,7 +13,6 @@ 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,116 +112,6 @@ 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

@@ -0,0 +1,63 @@
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('\'', '"');
}
}

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,12 +56,9 @@ namespace GitHub.Runner.Common.Tests
}
var traceListener = new HostTraceListener(TraceFileName);
var encoders = new List<ValueEncoder>()
{
ValueEncoders.JsonStringEscape,
ValueEncoders.UriDataEscape
};
_secretMasker = new SecretMasker(encoders);
_secretMasker = new SecretMasker();
_secretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
_secretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
_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,5 +89,128 @@ 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 +1 @@
2.303.0
2.304.1