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
|
## Features
|
||||||
- Runner changes for communication with Results service (#2510, #2531, #2535, #2516)
|
- Support matrix context in output keys (#2477)
|
||||||
- Add `*.ghe.localhost` domains to hosted server check (#2536)
|
- Add update certificates to `./run.sh` if `RUNNER_UPDATE_CA_CERTS` env is set (#2471)
|
||||||
- Add `OrchestrationId` to user-agent for better telemetry correlation. (#2568)
|
- Bypass all proxies for all hosts if `no_proxy='*'` is set (#2395)
|
||||||
- Add warning to notify about forcing actions to run on node16 instead of node12 (#2678)
|
- Change runner image to make user/folder align with `ubuntu-latest` hosted runner. (#2469)
|
||||||
|
|
||||||
## Bugs
|
## Bugs
|
||||||
- Fix JIT configurations on Windows (#2497)
|
- Exit on runner version deprecation error (#2299)
|
||||||
- Guard against NullReference while creating HostContext (#2343)
|
- Runner service exit after consecutive re-try exits (#2426)
|
||||||
- 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
|
## Misc
|
||||||
- Bump container hooks version to 0.3.1 in runner image (#2496)
|
- Replace deprecated command with environment file (#2429)
|
||||||
- Runner changes to communicate with vNext services (#2487, #2500, #2505, #2541, #2547)
|
- 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.
|
_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.
|
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.
|
# Download from source to the partial file.
|
||||||
echo "Downloading $download_source"
|
echo "Downloading $download_source"
|
||||||
mkdir -p "$(dirname "$download_target")" || checkRC 'mkdir'
|
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)
|
# curl -f Fail silently (no output at all) on HTTP errors (H)
|
||||||
# -k Allow connections to SSL sites without certs (H)
|
# -k Allow connections to SSL sites without certs (H)
|
||||||
# -S Show error. With -s, make curl show errors when they occur
|
# -S Show error. With -s, make curl show errors when they occur
|
||||||
# -L Follow redirects (H)
|
# -L Follow redirects (H)
|
||||||
# -o FILE Write to FILE instead of stdout
|
# -o FILE Write to FILE instead of stdout
|
||||||
# --retry 3 Retries transient errors 3 times (timeouts, 5xx)
|
curl -fkSL -o "$partial_target" "$download_source" 2>"${download_target}_download.log" || checkRC 'curl'
|
||||||
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.
|
# Move the partial file to the download target.
|
||||||
mv "$partial_target" "$download_target" || checkRC 'mv'
|
mv "$partial_target" "$download_target" || checkRC 'mv'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
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 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 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 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
|
public static class RunnerEvent
|
||||||
@@ -263,7 +261,6 @@ namespace GitHub.Runner.Common
|
|||||||
public static readonly string AccessToken = "system.accessToken";
|
public static readonly string AccessToken = "system.accessToken";
|
||||||
public static readonly string Culture = "system.culture";
|
public static readonly string Culture = "system.culture";
|
||||||
public static readonly string PhaseDisplayName = "system.phaseDisplayName";
|
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 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, object> _serviceInstances = new();
|
||||||
private readonly ConcurrentDictionary<Type, Type> _serviceTypes = 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 readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
|
||||||
private CancellationTokenSource _runnerShutdownTokenSource = new();
|
private CancellationTokenSource _runnerShutdownTokenSource = new();
|
||||||
private object _perfLock = new();
|
private object _perfLock = new();
|
||||||
@@ -82,17 +82,20 @@ namespace GitHub.Runner.Common
|
|||||||
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
|
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
|
||||||
_loadContext.Unloading += LoadContext_Unloading;
|
_loadContext.Unloading += LoadContext_Unloading;
|
||||||
|
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscape);
|
var masks = new List<ValueEncoder>()
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift1);
|
{
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift2);
|
ValueEncoders.EnumerateBase64Variations,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.CommandLineArgumentEscape);
|
ValueEncoders.CommandLineArgumentEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.ExpressionStringEscape);
|
ValueEncoders.ExpressionStringEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
|
ValueEncoders.JsonStringEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
|
ValueEncoders.UriDataEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape);
|
ValueEncoders.XmlDataEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.TrimDoubleQuotes);
|
ValueEncoders.TrimDoubleQuotes,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPreAmpersandEscape);
|
ValueEncoders.PowerShellPreAmpersandEscape,
|
||||||
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPostAmpersandEscape);
|
ValueEncoders.PowerShellPostAmpersandEscape
|
||||||
|
};
|
||||||
|
_secretMasker = new SecretMasker(masks);
|
||||||
|
|
||||||
|
|
||||||
// Create StdoutTraceListener if ENV is set
|
// Create StdoutTraceListener if ENV is set
|
||||||
StdoutTraceListener stdoutTraceListener = null;
|
StdoutTraceListener stdoutTraceListener = null;
|
||||||
|
|||||||
@@ -199,15 +199,13 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
Trace.Info($"Attempting to start websocket client with delay {delay}.");
|
Trace.Info($"Attempting to start websocket client with delay {delay}.");
|
||||||
await Task.Delay(delay);
|
await Task.Delay(delay);
|
||||||
using var connectTimeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), default(CancellationToken));
|
||||||
await this._websocketClient.ConnectAsync(new Uri(feedStreamUrl), connectTimeoutTokenSource.Token);
|
|
||||||
Trace.Info($"Successfully started websocket client.");
|
Trace.Info($"Successfully started websocket client.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Trace.Info("Exception caught during websocket client connect, fallback of HTTP would be used now instead of websocket.");
|
Trace.Info("Exception caught during websocket client connect, fallback of HTTP would be used now instead of websocket.");
|
||||||
Trace.Error(ex);
|
Trace.Error(ex);
|
||||||
this._websocketClient = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace GitHub.Runner.Common
|
|||||||
TaskCompletionSource<int> JobRecordUpdated { get; }
|
TaskCompletionSource<int> JobRecordUpdated { get; }
|
||||||
event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
|
event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
|
||||||
Task ShutdownAsync();
|
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 QueueWebConsoleLine(Guid stepRecordId, string line, long? lineNumber = null);
|
||||||
void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource);
|
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);
|
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> _jobCompletionSource = new();
|
||||||
private readonly TaskCompletionSource<int> _jobRecordUpdated = new();
|
private readonly TaskCompletionSource<int> _jobRecordUpdated = new();
|
||||||
private bool _queueInProcess = false;
|
private bool _queueInProcess = false;
|
||||||
private bool _resultsServiceOnly = false;
|
|
||||||
|
|
||||||
public TaskCompletionSource<int> JobRecordUpdated => _jobRecordUpdated;
|
public TaskCompletionSource<int> JobRecordUpdated => _jobRecordUpdated;
|
||||||
|
|
||||||
@@ -96,17 +95,13 @@ namespace GitHub.Runner.Common
|
|||||||
_resultsServer = hostContext.GetService<IResultsServer>();
|
_resultsServer = hostContext.GetService<IResultsServer>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start(Pipelines.AgentJobRequestMessage jobRequest, bool resultServiceOnly = false)
|
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
|
||||||
{
|
{
|
||||||
Trace.Entering();
|
Trace.Entering();
|
||||||
_resultsServiceOnly = resultServiceOnly;
|
|
||||||
|
|
||||||
var serviceEndPoint = jobRequest.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
var serviceEndPoint = jobRequest.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (!resultServiceOnly)
|
_jobServer.InitializeWebsocketClient(serviceEndPoint);
|
||||||
{
|
|
||||||
_jobServer.InitializeWebsocketClient(serviceEndPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This code is usually wrapped by an instance of IExecutionContext which isn't available here.
|
// 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);
|
jobRequest.Variables.TryGetValue("system.github.results_endpoint", out VariableValue resultsEndpointVariable);
|
||||||
@@ -117,16 +112,8 @@ namespace GitHub.Runner.Common
|
|||||||
!string.IsNullOrEmpty(accessToken) &&
|
!string.IsNullOrEmpty(accessToken) &&
|
||||||
!string.IsNullOrEmpty(resultsReceiverEndpoint))
|
!string.IsNullOrEmpty(resultsReceiverEndpoint))
|
||||||
{
|
{
|
||||||
string liveConsoleFeedUrl = null;
|
|
||||||
Trace.Info("Initializing results client");
|
Trace.Info("Initializing results client");
|
||||||
if (resultServiceOnly
|
_resultsServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), accessToken);
|
||||||
&& serviceEndPoint.Data.TryGetValue("FeedStreamUrl", out var feedStreamUrl)
|
|
||||||
&& !string.IsNullOrEmpty(feedStreamUrl))
|
|
||||||
{
|
|
||||||
liveConsoleFeedUrl = feedStreamUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
_resultsServer.InitializeResultsClient(new Uri(resultsReceiverEndpoint), liveConsoleFeedUrl, accessToken);
|
|
||||||
_resultsClientInitiated = true;
|
_resultsClientInitiated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,9 +194,6 @@ namespace GitHub.Runner.Common
|
|||||||
Trace.Info($"Disposing job server ...");
|
Trace.Info($"Disposing job server ...");
|
||||||
await _jobServer.DisposeAsync();
|
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.");
|
Trace.Info("All queue process tasks have been stopped, and all queues are drained.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,14 +372,7 @@ namespace GitHub.Runner.Common
|
|||||||
// Give at most 60s for each request.
|
// Give at most 60s for each request.
|
||||||
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
|
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
|
||||||
{
|
{
|
||||||
if (_resultsServiceOnly)
|
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch.Select(logLine => logLine.Line).ToList(), batch[0].LineNumber, timeoutTokenSource.Token);
|
||||||
{
|
|
||||||
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)
|
if (_firstConsoleOutputs)
|
||||||
@@ -622,7 +599,7 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
foreach (var detailTimeline in update.PendingRecords.Where(r => r.Details != null))
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -644,11 +621,7 @@ namespace GitHub.Runner.Common
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_resultsServiceOnly)
|
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
|
||||||
{
|
|
||||||
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_resultsClientInitiated)
|
if (_resultsClientInitiated)
|
||||||
@@ -824,30 +797,27 @@ namespace GitHub.Runner.Common
|
|||||||
bool uploadSucceed = false;
|
bool uploadSucceed = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_resultsServiceOnly)
|
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
|
// Create the log
|
||||||
|
var taskLog = await _jobServer.CreateLogAsync(_scopeIdentifier, _hubName, _planId, new TaskLog(String.Format(@"logs\{0:D}", file.TimelineRecordId)), default(CancellationToken));
|
||||||
|
|
||||||
|
// Upload the contents
|
||||||
|
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
{
|
{
|
||||||
// Create the log
|
var logUploaded = await _jobServer.AppendLogContentAsync(_scopeIdentifier, _hubName, _planId, taskLog.Id, fs, default(CancellationToken));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// Create a new record and only set the Log field
|
||||||
|
var attachmentUpdataRecord = new TimelineRecord() { Id = file.TimelineRecordId, Log = taskLog };
|
||||||
|
QueueTimelineRecordUpdate(file.TimelineId, attachmentUpdataRecord);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create attachment
|
||||||
|
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||||
{
|
{
|
||||||
// Create attachment
|
var result = await _jobServer.CreateAttachmentAsync(_scopeIdentifier, _hubName, _planId, file.TimelineId, file.TimelineRecordId, file.Type, file.Name, fs, default(CancellationToken));
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Security;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
using GitHub.Runner.Sdk;
|
|
||||||
using GitHub.Services.Common;
|
|
||||||
using GitHub.Services.Results.Client;
|
using GitHub.Services.Results.Client;
|
||||||
using GitHub.Services.WebApi.Utilities.Internal;
|
|
||||||
|
|
||||||
namespace GitHub.Runner.Common
|
namespace GitHub.Runner.Common
|
||||||
{
|
{
|
||||||
[ServiceLocator(Default = typeof(ResultServer))]
|
[ServiceLocator(Default = typeof(ResultServer))]
|
||||||
public interface IResultsServer : IRunnerService, IAsyncDisposable
|
public interface IResultsServer : IRunnerService
|
||||||
{
|
{
|
||||||
void InitializeResultsClient(Uri uri, string liveConsoleFeedUrl, string token);
|
void InitializeResultsClient(Uri uri, 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
|
// logging and console
|
||||||
Task CreateResultsStepSummaryAsync(string planId, string jobId, Guid stepId, string file,
|
Task CreateResultsStepSummaryAsync(string planId, string jobId, Guid stepId, string file,
|
||||||
@@ -40,26 +31,10 @@ namespace GitHub.Runner.Common
|
|||||||
{
|
{
|
||||||
private ResultsHttpClient _resultsClient;
|
private ResultsHttpClient _resultsClient;
|
||||||
|
|
||||||
private ClientWebSocket _websocketClient;
|
public void InitializeResultsClient(Uri uri, string token)
|
||||||
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();
|
var httpMessageHandler = HostContext.CreateHttpClientHandler();
|
||||||
this._resultsClient = new ResultsHttpClient(uri, httpMessageHandler, token, disposeHandler: true);
|
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,
|
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.");
|
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)
|
public Task<AgentJobRequestMessage> GetJobMessageAsync(string id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return RetryRequest<AgentJobRequestMessage>(
|
var jobMessage = RetryRequest<AgentJobRequestMessage>(
|
||||||
async () => await _runServiceHttpClient.GetJobMessageAsync(requestUri, id, cancellationToken), cancellationToken);
|
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)
|
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)
|
public Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return RetryRequest<RenewJobResponse>(
|
var renewJobResponse = RetryRequest<RenewJobResponse>(
|
||||||
async () => await _runServiceHttpClient.RenewJobAsync(requestUri, planId, jobId, cancellationToken), cancellationToken);
|
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
|
namespace GitHub.Runner.Sdk
|
||||||
{
|
{
|
||||||
@@ -11,11 +11,10 @@ namespace GitHub.Runner.Sdk
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
string.Equals(gitHubUrl.Host, "github.com", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(gitHubUrl.Host, "github.com", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(gitHubUrl.Host, "www.github.com", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(gitHubUrl.Host, "www.github.com", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(gitHubUrl.Host, "github.localhost", StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(gitHubUrl.Host, "github.localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||||
gitHubUrl.Host.EndsWith(".ghe.localhost", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
gitHubUrl.Host.EndsWith(".ghe.com", StringComparison.OrdinalIgnoreCase);
|
gitHubUrl.Host.EndsWith(".ghe.com", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ namespace GitHub.Runner.Sdk
|
|||||||
trace?.Verbose(ex.ToString());
|
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()}'");
|
trace?.Info($"Location: '{matches.First()}'");
|
||||||
return matches.First();
|
return matches.First();
|
||||||
@@ -86,7 +86,7 @@ namespace GitHub.Runner.Sdk
|
|||||||
for (int i = 0; i < pathExtSegments.Length; i++)
|
for (int i = 0; i < pathExtSegments.Length; i++)
|
||||||
{
|
{
|
||||||
string fullPath = Path.Combine(pathSegment, $"{command}{pathExtSegments[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}'");
|
trace?.Info($"Location: '{fullPath}'");
|
||||||
return fullPath;
|
return fullPath;
|
||||||
@@ -105,7 +105,7 @@ namespace GitHub.Runner.Sdk
|
|||||||
trace?.Verbose(ex.ToString());
|
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()}'");
|
trace?.Info($"Location: '{matches.First()}'");
|
||||||
return matches.First();
|
return matches.First();
|
||||||
@@ -128,15 +128,5 @@ namespace GitHub.Runner.Sdk
|
|||||||
|
|
||||||
return null;
|
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;
|
bool isOptOut = isWorkflowOptOutSet ? StringUtil.ConvertToBoolean(workflowOptOut) : isLocalOptOut;
|
||||||
if (!isOptOut)
|
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";
|
nodeData.NodeVersion = "node16";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -43,13 +42,6 @@ namespace GitHub.Runner.Worker
|
|||||||
DateTime jobStartTimeUtc = DateTime.UtcNow;
|
DateTime jobStartTimeUtc = DateTime.UtcNow;
|
||||||
IRunnerService server = null;
|
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));
|
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
if (MessageUtil.IsRunServiceJob(message.MessageType))
|
if (MessageUtil.IsRunServiceJob(message.MessageType))
|
||||||
{
|
{
|
||||||
@@ -57,9 +49,6 @@ namespace GitHub.Runner.Worker
|
|||||||
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
VssCredentials jobServerCredential = VssUtil.GetVssCredential(systemConnection);
|
||||||
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
await runServer.ConnectAsync(systemConnection.Url, jobServerCredential);
|
||||||
server = runServer;
|
server = runServer;
|
||||||
|
|
||||||
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
|
|
||||||
_jobServerQueue.Start(message, resultServiceOnly: true);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -348,12 +337,6 @@ namespace GitHub.Runner.Worker
|
|||||||
jobContext.Warning(string.Format(Constants.Runner.Node12DetectedAfterEndOfLife, actions));
|
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
|
try
|
||||||
{
|
{
|
||||||
await ShutdownQueue(throwOnFailure: true);
|
await ShutdownQueue(throwOnFailure: true);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using GitHub.DistributedTask.Logging;
|
using GitHub.DistributedTask.Logging;
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Expressions2.Sdk
|
namespace GitHub.DistributedTask.Expressions2.Sdk
|
||||||
@@ -55,7 +55,7 @@ namespace GitHub.DistributedTask.Expressions2.Sdk
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Evaluate
|
// Evaluate
|
||||||
secretMasker = secretMasker?.Clone() ?? new SecretMasker();
|
secretMasker = secretMasker?.Clone() ?? new SecretMasker(Enumerable.Empty<ValueEncoder>());
|
||||||
trace = new EvaluationTraceWriter(trace, secretMasker);
|
trace = new EvaluationTraceWriter(trace, secretMasker);
|
||||||
var context = new EvaluationContext(trace, secretMasker, state, options, this);
|
var context = new EvaluationContext(trace, secretMasker, state, options, this);
|
||||||
trace.Info($"Evaluating: {ConvertToExpression()}");
|
trace.Info($"Evaluating: {ConvertToExpression()}");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Logging
|
namespace GitHub.DistributedTask.Logging
|
||||||
@@ -8,7 +8,6 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
{
|
{
|
||||||
void AddRegex(String pattern);
|
void AddRegex(String pattern);
|
||||||
void AddValue(String value);
|
void AddValue(String value);
|
||||||
void AddValueEncoder(ValueEncoder encoder);
|
|
||||||
ISecretMasker Clone();
|
ISecretMasker Clone();
|
||||||
String MaskSecrets(String input);
|
String MaskSecrets(String input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -10,11 +10,11 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public sealed class SecretMasker : ISecretMasker, IDisposable
|
public sealed class SecretMasker : ISecretMasker, IDisposable
|
||||||
{
|
{
|
||||||
public SecretMasker()
|
public SecretMasker(IEnumerable<ValueEncoder> encoders)
|
||||||
{
|
{
|
||||||
m_originalValueSecrets = new HashSet<ValueSecret>();
|
m_originalValueSecrets = new HashSet<ValueSecret>();
|
||||||
m_regexSecrets = new HashSet<RegexSecret>();
|
m_regexSecrets = new HashSet<RegexSecret>();
|
||||||
m_valueEncoders = new HashSet<ValueEncoder>();
|
m_valueEncoders = new HashSet<ValueEncoder>(encoders ?? Enumerable.Empty<ValueEncoder>());
|
||||||
m_valueSecrets = new HashSet<ValueSecret>();
|
m_valueSecrets = new HashSet<ValueSecret>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,15 +104,11 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the encoded values.
|
var secretVariations = valueEncoders.SelectMany(encoder => encoder(value))
|
||||||
foreach (ValueEncoder valueEncoder in valueEncoders)
|
.Where(variation => !string.IsNullOrEmpty(variation))
|
||||||
{
|
.Distinct()
|
||||||
String encodedValue = valueEncoder(value);
|
.Select(variation => new ValueSecret(variation));
|
||||||
if (!String.IsNullOrEmpty(encodedValue))
|
valueSecrets.AddRange(secretVariations);
|
||||||
{
|
|
||||||
valueSecrets.Add(new ValueSecret(encodedValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write section.
|
// Write section.
|
||||||
try
|
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 ISecretMasker Clone() => new SecretMasker(this);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -1,73 +1,97 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace GitHub.DistributedTask.Logging
|
namespace GitHub.DistributedTask.Logging
|
||||||
{
|
{
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public delegate String ValueEncoder(String value);
|
public delegate IEnumerable<string> ValueEncoder(string value);
|
||||||
|
|
||||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
public static class ValueEncoders
|
public static class ValueEncoders
|
||||||
{
|
{
|
||||||
public static String Base64StringEscape(String value)
|
|
||||||
{
|
|
||||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base64 is 6 bits -> char
|
public static IEnumerable<string> EnumerateBase64Variations(string value)
|
||||||
// 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);
|
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:
|
||||||
return Base64StringEscapeShift(value, 2);
|
// "YWJj" when aligned
|
||||||
|
// ".!FiYw==" when preceded by 3x + 1 bytes
|
||||||
|
// "..!hYmM=" when preceded by 3x + 2 bytes
|
||||||
|
// (where . represents an unrelated Base64 digit, ! represents a Base64 digit that should be masked, and x represents any non-negative integer)
|
||||||
|
|
||||||
|
var rawBytes = Encoding.UTF8.GetBytes(value);
|
||||||
|
|
||||||
|
for (var offset = 0; offset <= 2; offset++)
|
||||||
|
{
|
||||||
|
var prunedBytes = rawBytes.Skip(offset).ToArray();
|
||||||
|
if (prunedBytes.Length > 0)
|
||||||
|
{
|
||||||
|
// Don't include Base64 padding characters (=) in Base64 representations of the secret.
|
||||||
|
// They don't represent anything interesting, so they don't need to be masked.
|
||||||
|
// (Some clients omit the padding, so we want to be sure we recognize the secret regardless of whether the padding is present or not.)
|
||||||
|
var buffer = new StringBuilder(Convert.ToBase64String(prunedBytes).TrimEnd(BASE64_PADDING_SUFFIX));
|
||||||
|
yield return buffer.ToString();
|
||||||
|
|
||||||
|
// Also, yield the RFC4648-equivalent RegEx.
|
||||||
|
buffer.Replace('+', '-');
|
||||||
|
buffer.Replace('/', '_');
|
||||||
|
yield return buffer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used when we pass environment variables to docker to escape " with \"
|
// 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.
|
// Convert to a JSON string and then remove the leading/trailing double-quote.
|
||||||
String jsonString = JsonConvert.ToString(value);
|
String jsonString = JsonConvert.ToString(value);
|
||||||
String jsonEscapedValue = jsonString.Substring(startIndex: 1, length: jsonString.Length - 2);
|
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;
|
var trimmed = string.Empty;
|
||||||
if (!string.IsNullOrEmpty(value) &&
|
if (!string.IsNullOrEmpty(value) &&
|
||||||
@@ -78,17 +102,17 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
trimmed = value.Substring(1, value.Length - 2);
|
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
|
// 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:
|
// The secret secretpart1&secretpart2&secretpart3 would be split into 2 sections:
|
||||||
// 'secretpart1&secretpart2&' and 'secretpart3'. This method masks for the first section.
|
// 'secretpart1&secretpart2&' and 'secretpart3'. This method masks for the first section.
|
||||||
|
|
||||||
// The secret secretpart1&+secretpart2&secretpart3 would be split into 2 sections:
|
// The secret secretpart1&+secretpart2&secretpart3 would be split into 2 sections:
|
||||||
// 'secretpart1&+' and (no 's') 'ecretpart2&secretpart3'. This method masks for the first section.
|
// 'secretpart1&+' and (no 's') 'ecretpart2&secretpart3'. This method masks for the first section.
|
||||||
|
|
||||||
@@ -112,10 +136,10 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmed;
|
yield return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String PowerShellPostAmpersandEscape(String value)
|
public static IEnumerable<string> PowerShellPostAmpersandEscape(string value)
|
||||||
{
|
{
|
||||||
var trimmed = string.Empty;
|
var trimmed = string.Empty;
|
||||||
if (!string.IsNullOrEmpty(value) && value.Contains("&"))
|
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)
|
private static string UriDataEscape(string value, Int32 maxSegmentSize)
|
||||||
{
|
|
||||||
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)
|
if (value.Length <= maxSegmentSize)
|
||||||
{
|
{
|
||||||
@@ -183,5 +190,7 @@ namespace GitHub.DistributedTask.Logging
|
|||||||
|
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const char BASE64_PADDING_SUFFIX = '=';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
|
using GitHub.DistributedTask.WebApi;
|
||||||
|
|
||||||
namespace GitHub.Actions.RunService.WebApi
|
namespace GitHub.Actions.RunService.WebApi
|
||||||
{
|
{
|
||||||
[DataContract]
|
[DataContract]
|
||||||
public class AcquireJobRequest
|
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)]
|
[DataMember(Name = "streamId", EmitDefaultValue = false)]
|
||||||
public string StreamId { get; set; }
|
public string StreamID { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -56,7 +55,7 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AgentJobRequestMessage> GetJobMessageAsync(
|
public Task<AgentJobRequestMessage> GetJobMessageAsync(
|
||||||
Uri requestUri,
|
Uri requestUri,
|
||||||
string messageId,
|
string messageId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -64,34 +63,20 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
HttpMethod httpMethod = new HttpMethod("POST");
|
HttpMethod httpMethod = new HttpMethod("POST");
|
||||||
var payload = new AcquireJobRequest
|
var payload = new AcquireJobRequest
|
||||||
{
|
{
|
||||||
JobMessageId = messageId,
|
StreamID = messageId
|
||||||
StreamId = messageId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
requestUri = new Uri(requestUri, "acquirejob");
|
requestUri = new Uri(requestUri, "acquirejob");
|
||||||
|
|
||||||
var requestContent = new ObjectContent<AcquireJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
var requestContent = new ObjectContent<AcquireJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
||||||
var result = await SendAsync<AgentJobRequestMessage>(
|
return SendAsync<AgentJobRequestMessage>(
|
||||||
httpMethod,
|
httpMethod,
|
||||||
requestUri: requestUri,
|
requestUri: requestUri,
|
||||||
content: requestContent,
|
content: requestContent,
|
||||||
cancellationToken: cancellationToken);
|
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,
|
Uri requestUri,
|
||||||
Guid planId,
|
Guid planId,
|
||||||
Guid jobId,
|
Guid jobId,
|
||||||
@@ -113,26 +98,14 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
requestUri = new Uri(requestUri, "completejob");
|
requestUri = new Uri(requestUri, "completejob");
|
||||||
|
|
||||||
var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
var requestContent = new ObjectContent<CompleteJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
||||||
var response = await SendAsync(
|
return SendAsync(
|
||||||
httpMethod,
|
httpMethod,
|
||||||
requestUri,
|
requestUri,
|
||||||
content: requestContent,
|
content: requestContent,
|
||||||
cancellationToken: cancellationToken);
|
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,
|
Uri requestUri,
|
||||||
Guid planId,
|
Guid planId,
|
||||||
Guid jobId,
|
Guid jobId,
|
||||||
@@ -148,24 +121,11 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
requestUri = new Uri(requestUri, "renewjob");
|
requestUri = new Uri(requestUri, "renewjob");
|
||||||
|
|
||||||
var requestContent = new ObjectContent<RenewJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
var requestContent = new ObjectContent<RenewJobRequest>(payload, new VssJsonMediaTypeFormatter(true));
|
||||||
var result = await SendAsync<RenewJobResponse>(
|
return SendAsync<RenewJobResponse>(
|
||||||
httpMethod,
|
httpMethod,
|
||||||
requestUri,
|
requestUri,
|
||||||
content: requestContent,
|
content: requestContent,
|
||||||
cancellationToken: cancellationToken);
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -56,7 +55,7 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TaskAgentMessage> GetRunnerMessageAsync(
|
public Task<TaskAgentMessage> GetRunnerMessageAsync(
|
||||||
string runnerVersion,
|
string runnerVersion,
|
||||||
TaskAgentStatus? status,
|
TaskAgentStatus? status,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
@@ -75,18 +74,11 @@ namespace GitHub.Actions.RunService.WebApi
|
|||||||
queryParams.Add("runnerVersion", runnerVersion);
|
queryParams.Add("runnerVersion", runnerVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await SendAsync<TaskAgentMessage>(
|
return SendAsync<TaskAgentMessage>(
|
||||||
new HttpMethod("GET"),
|
new HttpMethod("GET"),
|
||||||
requestUri: requestUri,
|
requestUri: requestUri,
|
||||||
queryParameters: queryParams,
|
queryParameters: queryParams,
|
||||||
cancellationToken: cancellationToken);
|
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;
|
public string StartedAt;
|
||||||
[DataMember]
|
[DataMember]
|
||||||
public string CompletedAt;
|
public string CompletedAt;
|
||||||
[DataMember]
|
|
||||||
public Conclusion Conclusion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Status
|
public enum Status
|
||||||
@@ -169,15 +167,6 @@ namespace GitHub.Services.Results.Contracts
|
|||||||
StatusCompleted = 6
|
StatusCompleted = 6
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Conclusion
|
|
||||||
{
|
|
||||||
ConclusionUnknown = 0,
|
|
||||||
ConclusionSuccess = 2,
|
|
||||||
ConclusionFailure = 3,
|
|
||||||
ConclusionCancelled = 4,
|
|
||||||
ConclusionSkipped = 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class BlobStorageTypes
|
public static class BlobStorageTypes
|
||||||
{
|
{
|
||||||
public static readonly string AzureBlobStorage = "BLOB_STORAGE_TYPE_AZURE";
|
public static readonly string AzureBlobStorage = "BLOB_STORAGE_TYPE_AZURE";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ using Newtonsoft.Json.Linq;
|
|||||||
|
|
||||||
namespace Sdk.WebApi.WebApi
|
namespace Sdk.WebApi.WebApi
|
||||||
{
|
{
|
||||||
public class RawHttpClientBase : IDisposable
|
public class RawHttpClientBase: IDisposable
|
||||||
{
|
{
|
||||||
protected RawHttpClientBase(
|
protected RawHttpClientBase(
|
||||||
Uri baseUrl,
|
Uri baseUrl,
|
||||||
@@ -101,7 +101,7 @@ namespace Sdk.WebApi.WebApi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task<RawHttpClientResult<T>> SendAsync<T>(
|
protected Task<T> SendAsync<T>(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
Uri requestUri,
|
Uri requestUri,
|
||||||
HttpContent content = null,
|
HttpContent content = null,
|
||||||
@@ -112,7 +112,7 @@ namespace Sdk.WebApi.WebApi
|
|||||||
return SendAsync<T>(method, null, requestUri, content, queryParameters, userState, cancellationToken);
|
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,
|
HttpMethod method,
|
||||||
IEnumerable<KeyValuePair<String, String>> additionalHeaders,
|
IEnumerable<KeyValuePair<String, String>> additionalHeaders,
|
||||||
Uri requestUri,
|
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,
|
HttpRequestMessage message,
|
||||||
Object userState = null,
|
Object userState = null,
|
||||||
CancellationToken cancellationToken = default(CancellationToken))
|
CancellationToken cancellationToken = default(CancellationToken))
|
||||||
@@ -138,16 +138,7 @@ namespace Sdk.WebApi.WebApi
|
|||||||
//from deadlocking...
|
//from deadlocking...
|
||||||
using (HttpResponseMessage response = await this.SendAsync(message, userState, cancellationToken).ConfigureAwait(false))
|
using (HttpResponseMessage response = await this.SendAsync(message, userState, cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (response.IsSuccessStatusCode)
|
return await ReadContentAsAsync<T>(response, cancellationToken).ConfigureAwait(false);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
Name = r.Name,
|
||||||
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
|
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
|
||||||
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat),
|
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat),
|
||||||
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat),
|
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat)
|
||||||
Conclusion = ConvertResultToConclusion(r.Result)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
public async Task UpdateWorkflowStepsAsync(Guid planId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat);
|
var timestamp = DateTime.UtcNow.ToString(Constants.TimestampFormat);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -13,6 +12,7 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
{
|
{
|
||||||
private HostContext _hc;
|
private HostContext _hc;
|
||||||
private CancellationTokenSource _tokenSource;
|
private CancellationTokenSource _tokenSource;
|
||||||
|
private const string EXPECTED_SECRET_MASK = "***";
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[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%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("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("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("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("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("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("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("YWJjZGU6***", _hc.SecretMasker.MaskSecrets(Convert.ToBase64String(Encoding.UTF8.GetBytes($"abcde:Password123!"))));
|
||||||
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!!123"));
|
Assert.Equal("123***123", _hc.SecretMasker.MaskSecrets("123Password123!!123"));
|
||||||
Assert.Equal("123short123", _hc.SecretMasker.MaskSecrets("123short123"));
|
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]
|
[Theory]
|
||||||
[InlineData("secret&secret&secret", "secret&secret&\x0033[96msecret\x0033[0m", "***\x0033[96m***\x0033[0m")]
|
[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")]
|
[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;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -56,9 +56,12 @@ namespace GitHub.Runner.Common.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
var traceListener = new HostTraceListener(TraceFileName);
|
var traceListener = new HostTraceListener(TraceFileName);
|
||||||
_secretMasker = new SecretMasker();
|
var encoders = new List<ValueEncoder>()
|
||||||
_secretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
|
{
|
||||||
_secretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
|
ValueEncoders.JsonStringEscape,
|
||||||
|
ValueEncoders.UriDataEscape
|
||||||
|
};
|
||||||
|
_secretMasker = new SecretMasker(encoders);
|
||||||
_traceManager = new TraceManager(traceListener, null, _secretMasker);
|
_traceManager = new TraceManager(traceListener, null, _secretMasker);
|
||||||
_trace = GetTrace(nameof(TestHostContext));
|
_trace = GetTrace(nameof(TestHostContext));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using GitHub.Runner.Common.Util;
|
using GitHub.Runner.Common.Util;
|
||||||
using GitHub.Runner.Sdk;
|
using GitHub.Runner.Sdk;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -89,128 +89,5 @@ namespace GitHub.Runner.Common.Tests.Util
|
|||||||
Assert.Equal(gitPath, gitPath2);
|
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