mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
1 Commits
fhammerl/c
...
Link-/ubun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c48dad21d |
@@ -9,7 +9,7 @@ ARG DOCKER_VERSION=20.10.23
|
||||
RUN apt update -y && apt install curl unzip -y
|
||||
|
||||
WORKDIR /actions-runner
|
||||
RUN curl -f -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
|
||||
RUN curl -f -L -o runner.tar.gz https://github.com/nebuchadnezzar-corp/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
|
||||
&& tar xzf ./runner.tar.gz \
|
||||
&& rm runner.tar.gz
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
## Features
|
||||
- Runner changes for communication with Results service (#2510, #2531, #2535, #2516)
|
||||
- Add `*.ghe.localhost` domains to hosted server check (#2536)
|
||||
- Add `OrchestrationId` to user-agent for better telemetry correlation. (#2568)
|
||||
|
||||
## Bugs
|
||||
- Fixes `if:cancelled()` composite steps not running and normal composite steps not interrupting when the job is cancelled (#2638)
|
||||
- Fix the bug causing double error reporting fix to remain inactive (#2703)
|
||||
- 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
|
||||
- Collect telemetry on GitHub-related HTTP requests (#2691)
|
||||
- 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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.307.0
|
||||
<Update to ./src/runnerversion when creating release>
|
||||
|
||||
@@ -155,7 +155,6 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
|
||||
public static readonly string Node12Warning = "DistributedTask.AddWarningToNode12Action";
|
||||
public static readonly string LogTemplateErrorsAsDebugMessages = "DistributedTask.LogTemplateErrorsAsDebugMessages";
|
||||
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
|
||||
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
|
||||
}
|
||||
@@ -172,8 +171,6 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string UnsupportedSummarySize = "$GITHUB_STEP_SUMMARY upload aborted, supports content up to a size of {0}k, got {1}k. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
|
||||
public static readonly string SummaryUploadError = "$GITHUB_STEP_SUMMARY upload aborted, an error occurred when uploading the summary. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
|
||||
public static readonly string Node12DetectedAfterEndOfLife = "Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: {0}. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.";
|
||||
public static readonly string EnforcedNode12DetectedAfterEndOfLife = "The following actions uses node12 which is deprecated and will be forced to run on node16: {0}. For more info: https://github.blog/changelog/2023-06-13-github-actions-all-actions-will-run-on-node16-instead-of-node12-by-default/";
|
||||
public static readonly string EnforcedNode12DetectedAfterEndOfLifeEnvVariable = "Node16ForceActionsWarnings";
|
||||
}
|
||||
|
||||
public static class RunnerEvent
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace GitHub.Runner.Common
|
||||
Dictionary<String, VariableValue> outputs,
|
||||
IList<StepResult> stepResults,
|
||||
IList<Annotation> jobAnnotations,
|
||||
string environmentUrl,
|
||||
CancellationToken token);
|
||||
|
||||
Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken token);
|
||||
@@ -71,12 +70,11 @@ namespace GitHub.Runner.Common
|
||||
Dictionary<String, VariableValue> outputs,
|
||||
IList<StepResult> stepResults,
|
||||
IList<Annotation> jobAnnotations,
|
||||
string environmentUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
CheckConnection();
|
||||
return RetryRequest(
|
||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, cancellationToken), cancellationToken);
|
||||
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken cancellationToken)
|
||||
|
||||
@@ -1077,7 +1077,23 @@ namespace GitHub.Runner.Listener
|
||||
|
||||
if (this._isRunServiceJob)
|
||||
{
|
||||
Trace.Verbose($"Skip CompleteJobRequestAsync call from Listener because it's RunService job");
|
||||
var runServer = await GetRunServerAsync(systemConnection);
|
||||
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
|
||||
var unhandledAnnotation = unhandledExceptionIssue.ToAnnotation();
|
||||
var jobAnnotations = new List<Annotation>();
|
||||
if (unhandledAnnotation.HasValue)
|
||||
{
|
||||
jobAnnotations.Add(unhandledAnnotation.Value);
|
||||
}
|
||||
try
|
||||
{
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error("Fail to raise job completion back to service.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1177,7 +1193,7 @@ namespace GitHub.Runner.Listener
|
||||
jobAnnotations.Add(unhandledAnnotation.Value);
|
||||
}
|
||||
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, CancellationToken.None);
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -69,10 +69,6 @@ namespace GitHub.Runner.Sdk
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(httpProxyAddress) && !Uri.TryCreate(httpProxyAddress, UriKind.Absolute, out var _))
|
||||
{
|
||||
httpProxyAddress = PrependHttpIfMissing(httpProxyAddress);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(httpProxyAddress) && Uri.TryCreate(httpProxyAddress, UriKind.Absolute, out var proxyHttpUri))
|
||||
{
|
||||
_httpProxyAddress = proxyHttpUri.OriginalString;
|
||||
@@ -103,10 +99,6 @@ namespace GitHub.Runner.Sdk
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(httpsProxyAddress) && !Uri.TryCreate(httpsProxyAddress, UriKind.Absolute, out var _))
|
||||
{
|
||||
httpsProxyAddress = PrependHttpIfMissing(httpsProxyAddress);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(httpsProxyAddress) && Uri.TryCreate(httpsProxyAddress, UriKind.Absolute, out var proxyHttpsUri))
|
||||
{
|
||||
_httpsProxyAddress = proxyHttpsUri.OriginalString;
|
||||
@@ -248,20 +240,5 @@ namespace GitHub.Runner.Sdk
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string PrependHttpIfMissing(string proxyAddress)
|
||||
{
|
||||
// much like in golang, see https://github.com/golang/net/blob/f5464ddb689c015d1abf4df78a806a54af977e6c/http/httpproxy/proxy.go#LL156C31-L156C31
|
||||
if (!proxyAddress.StartsWith("http://", StringComparison.Ordinal) && !proxyAddress.StartsWith("https://", StringComparison.Ordinal))
|
||||
{
|
||||
var prependedProxyAddress = "http://" + proxyAddress;
|
||||
if (Uri.TryCreate(prependedProxyAddress, UriKind.Absolute, out var _))
|
||||
{
|
||||
// if prepending http:// turns the proxyAddress into a valid Uri, then use that
|
||||
return prependedProxyAddress;
|
||||
}
|
||||
}
|
||||
return proxyAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace GitHub.Runner.Sdk
|
||||
|
||||
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("USE_BROKER_FLOW")))
|
||||
{
|
||||
settings.AllowAutoRedirectForBroker = true;
|
||||
settings.AllowAutoRedirect = true;
|
||||
}
|
||||
|
||||
// Remove Invariant from the list of accepted languages.
|
||||
|
||||
@@ -1042,7 +1042,7 @@ namespace GitHub.Runner.Worker
|
||||
if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Container)
|
||||
{
|
||||
var containerAction = actionDefinitionData.Execution as ContainerActionExecutionData;
|
||||
if (DockerUtil.IsDockerfile(containerAction.Image))
|
||||
if (containerAction.Image.EndsWith("Dockerfile") || containerAction.Image.EndsWith("dockerfile"))
|
||||
{
|
||||
var dockerFileFullPath = Path.Combine(actionEntryDirectory, containerAction.Image);
|
||||
executionContext.Debug($"Dockerfile for action: '{dockerFileFullPath}'.");
|
||||
@@ -1127,16 +1127,8 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
else
|
||||
{
|
||||
var reference = repositoryReference.Name;
|
||||
if (!string.IsNullOrEmpty(repositoryReference.Path))
|
||||
{
|
||||
reference = $"{reference}/{repositoryReference.Path}";
|
||||
}
|
||||
if (!string.IsNullOrEmpty(repositoryReference.Ref))
|
||||
{
|
||||
reference = $"{reference}@{repositoryReference.Ref}";
|
||||
}
|
||||
throw new InvalidOperationException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' for action '{reference}'.");
|
||||
var fullPath = IOUtil.ResolvePath(actionEntryDirectory, "."); // resolve full path without access filesystem.
|
||||
throw new InvalidOperationException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container
|
||||
@@ -66,16 +65,6 @@ namespace GitHub.Runner.Worker.Container
|
||||
return "";
|
||||
}
|
||||
|
||||
public static bool IsDockerfile(string image)
|
||||
{
|
||||
if (image.StartsWith("docker://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var imageWithoutPath = image.Split('/').Last();
|
||||
return imageWithoutPath.StartsWith("Dockerfile.", StringComparison.OrdinalIgnoreCase) || imageWithoutPath.EndsWith("Dockerfile", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string CreateEscapedOption(string flag, string key)
|
||||
{
|
||||
if (String.IsNullOrEmpty(key))
|
||||
|
||||
@@ -78,12 +78,11 @@ namespace GitHub.Runner.Worker
|
||||
List<string> StepEnvironmentOverrides { get; }
|
||||
|
||||
ExecutionContext Root { get; }
|
||||
ExecutionContext Parent { get; }
|
||||
|
||||
// Initialize
|
||||
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
|
||||
void CancelToken();
|
||||
IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null, TimeSpan? timeout = null);
|
||||
IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null);
|
||||
IExecutionContext CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null);
|
||||
|
||||
// logging
|
||||
@@ -265,14 +264,6 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
public ExecutionContext Parent
|
||||
{
|
||||
get
|
||||
{
|
||||
return _parentExecutionContext;
|
||||
}
|
||||
}
|
||||
|
||||
public JobContext JobContext
|
||||
{
|
||||
get
|
||||
@@ -357,8 +348,7 @@ namespace GitHub.Runner.Worker
|
||||
bool isEmbedded = false,
|
||||
CancellationTokenSource cancellationTokenSource = null,
|
||||
Guid embeddedId = default(Guid),
|
||||
string siblingScopeName = null,
|
||||
TimeSpan? timeout = null)
|
||||
string siblingScopeName = null)
|
||||
{
|
||||
Trace.Entering();
|
||||
|
||||
@@ -387,12 +377,6 @@ namespace GitHub.Runner.Worker
|
||||
child.ExpressionFunctions.Add(item);
|
||||
}
|
||||
child._cancellationTokenSource = cancellationTokenSource ?? new CancellationTokenSource();
|
||||
if (timeout != null)
|
||||
{
|
||||
// composite steps inherit the timeout from the parent, set by https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes
|
||||
child.SetTimeout(timeout);
|
||||
}
|
||||
|
||||
child.EchoOnActionCommand = EchoOnActionCommand;
|
||||
|
||||
if (recordOrder != null)
|
||||
@@ -422,7 +406,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
/// <summary>
|
||||
/// An embedded execution context shares the same record ID, record name, logger,
|
||||
/// but NOT the cancellation token (just like workflow steps contexts - they don't share a token)
|
||||
/// and a linked cancellation token.
|
||||
/// </summary>
|
||||
public IExecutionContext CreateEmbeddedChild(
|
||||
string scopeName,
|
||||
@@ -432,7 +416,7 @@ namespace GitHub.Runner.Worker
|
||||
Dictionary<string, string> intraActionState = null,
|
||||
string siblingScopeName = null)
|
||||
{
|
||||
return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, cancellationTokenSource: null, intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName, timeout: GetRemainingTimeout());
|
||||
return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, cancellationTokenSource: CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token), intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName);
|
||||
}
|
||||
|
||||
public void Start(string currentOperation = null)
|
||||
@@ -493,32 +477,28 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
PublishStepTelemetry();
|
||||
|
||||
if (_record.RecordType == "Task")
|
||||
var stepResult = new StepResult
|
||||
{
|
||||
var stepResult = new StepResult
|
||||
ExternalID = _record.Id,
|
||||
Conclusion = _record.Result ?? TaskResult.Succeeded,
|
||||
Status = _record.State,
|
||||
Number = _record.Order,
|
||||
Name = _record.Name,
|
||||
StartedAt = _record.StartTime,
|
||||
CompletedAt = _record.FinishTime,
|
||||
Annotations = new List<Annotation>()
|
||||
};
|
||||
|
||||
_record.Issues?.ForEach(issue =>
|
||||
{
|
||||
var annotation = issue.ToAnnotation();
|
||||
if (annotation != null)
|
||||
{
|
||||
ExternalID = _record.Id,
|
||||
Conclusion = _record.Result ?? TaskResult.Succeeded,
|
||||
Status = _record.State,
|
||||
Number = _record.Order,
|
||||
Name = _record.Name,
|
||||
StartedAt = _record.StartTime,
|
||||
CompletedAt = _record.FinishTime,
|
||||
Annotations = new List<Annotation>()
|
||||
};
|
||||
|
||||
_record.Issues?.ForEach(issue =>
|
||||
{
|
||||
var annotation = issue.ToAnnotation();
|
||||
if (annotation != null)
|
||||
{
|
||||
stepResult.Annotations.Add(annotation.Value);
|
||||
}
|
||||
});
|
||||
|
||||
Global.StepsResult.Add(stepResult);
|
||||
}
|
||||
stepResult.Annotations.Add(annotation.Value);
|
||||
}
|
||||
});
|
||||
|
||||
Global.StepsResult.Add(stepResult);
|
||||
|
||||
if (Root != this)
|
||||
{
|
||||
@@ -613,33 +593,9 @@ namespace GitHub.Runner.Worker
|
||||
if (timeout != null)
|
||||
{
|
||||
_cancellationTokenSource.CancelAfter(timeout.Value);
|
||||
m_timeoutStartedAt = DateTime.UtcNow;
|
||||
m_timeout = timeout.Value;
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? m_timeoutStartedAt;
|
||||
TimeSpan? m_timeout;
|
||||
public TimeSpan? GetRemainingTimeout()
|
||||
{
|
||||
if (m_timeoutStartedAt != null && m_timeout != null)
|
||||
{
|
||||
var elapsedSinceTimeoutSet = DateTime.UtcNow - m_timeoutStartedAt.Value;
|
||||
var remainingTimeout = m_timeout.Value - elapsedSinceTimeoutSet;
|
||||
if (remainingTimeout.Ticks > 0)
|
||||
{
|
||||
return remainingTimeout;
|
||||
}
|
||||
else
|
||||
{
|
||||
// there was a timeout and it has expired
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
// no timeout was ever set
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Progress(int percentage, string currentOperation = null)
|
||||
{
|
||||
if (percentage > 100 || percentage < 0)
|
||||
@@ -1399,12 +1355,6 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
foreach (var key in dict.Keys.ToList())
|
||||
{
|
||||
if (key == PipelineTemplateConstants.HostWorkspace)
|
||||
{
|
||||
// The HostWorkspace context var is excluded so that there is a var that always points to the host path.
|
||||
// This var can be used to translate back from container paths, e.g. in HashFilesFunction, which always runs on the host machine
|
||||
continue;
|
||||
}
|
||||
if (dict[key] is StringContextData)
|
||||
{
|
||||
var value = dict[key].ToString();
|
||||
@@ -1451,15 +1401,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public void Error(string format, params Object[] args)
|
||||
{
|
||||
/* TraceWriter should be used for logging and not creating erros. */
|
||||
if (logTemplateErrorsAsDebugMessages())
|
||||
{
|
||||
_executionContext.Debug(string.Format(CultureInfo.CurrentCulture, format, args));
|
||||
}
|
||||
else
|
||||
{
|
||||
_executionContext.Error(string.Format(CultureInfo.CurrentCulture, format, args));
|
||||
}
|
||||
_executionContext.Error(string.Format(CultureInfo.CurrentCulture, format, args));
|
||||
}
|
||||
|
||||
public void Info(string format, params Object[] args)
|
||||
@@ -1472,18 +1414,8 @@ namespace GitHub.Runner.Worker
|
||||
// todo: switch to verbose?
|
||||
_executionContext.Debug(string.Format(CultureInfo.CurrentCulture, $"{format}", args));
|
||||
}
|
||||
|
||||
private bool logTemplateErrorsAsDebugMessages()
|
||||
{
|
||||
if (_executionContext.Global.Variables.TryGetValue(Constants.Runner.Features.LogTemplateErrorsAsDebugMessages, out var logErrorsAsDebug))
|
||||
{
|
||||
return StringUtil.ConvertToBoolean(logErrorsAsDebug, defaultValue: false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class WellKnownTags
|
||||
{
|
||||
public static readonly string Section = "##[section]";
|
||||
|
||||
@@ -26,18 +26,11 @@ namespace GitHub.Runner.Worker.Expressions
|
||||
ArgUtil.NotNull(githubContextData, nameof(githubContextData));
|
||||
var githubContext = githubContextData as DictionaryContextData;
|
||||
ArgUtil.NotNull(githubContext, nameof(githubContext));
|
||||
|
||||
if (!githubContext.TryGetValue(PipelineTemplateConstants.HostWorkspace, out var workspace))
|
||||
{
|
||||
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out workspace);
|
||||
}
|
||||
ArgUtil.NotNull(workspace, nameof(workspace));
|
||||
|
||||
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out var workspace);
|
||||
var workspaceData = workspace as StringContextData;
|
||||
ArgUtil.NotNull(workspaceData, nameof(workspaceData));
|
||||
|
||||
string githubWorkspace = workspaceData.Value;
|
||||
|
||||
bool followSymlink = false;
|
||||
List<string> patterns = new();
|
||||
var firstParameter = true;
|
||||
|
||||
@@ -322,9 +322,21 @@ namespace GitHub.Runner.Worker
|
||||
var equalsIndex = line.IndexOf("=", StringComparison.Ordinal);
|
||||
var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal);
|
||||
|
||||
// Heredoc style NAME<<EOF (where EOF is typically randomly-generated Base64 and may include an '=' character)
|
||||
// see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
if (heredocIndex >= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex))
|
||||
// Normal style NAME=VALUE
|
||||
if (equalsIndex >= 0 && (heredocIndex < 0 || equalsIndex < heredocIndex))
|
||||
{
|
||||
var split = line.Split(new[] { '=' }, 2, StringSplitOptions.None);
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
throw new Exception($"Invalid format '{line}'. Name must not be empty");
|
||||
}
|
||||
|
||||
key = split[0];
|
||||
output = split[1];
|
||||
}
|
||||
|
||||
// Heredoc style NAME<<EOF
|
||||
else if (heredocIndex >= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex))
|
||||
{
|
||||
var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None);
|
||||
if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1]))
|
||||
@@ -352,18 +364,6 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
output = endIndex > startIndex ? text.Substring(startIndex, endIndex - startIndex) : string.Empty;
|
||||
}
|
||||
// Normal style NAME=VALUE
|
||||
else if (equalsIndex >= 0 && heredocIndex < 0)
|
||||
{
|
||||
var split = line.Split(new[] { '=' }, 2, StringSplitOptions.None);
|
||||
if (string.IsNullOrEmpty(line))
|
||||
{
|
||||
throw new Exception($"Invalid format '{line}'. Name must not be empty");
|
||||
}
|
||||
|
||||
key = split[0];
|
||||
output = split[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Invalid format '{line}'");
|
||||
|
||||
@@ -310,7 +310,6 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
// Mark job as cancelled
|
||||
ExecutionContext.Root.Result = TaskResult.Canceled;
|
||||
ExecutionContext.Root.JobContext.Status = ExecutionContext.Root.Result?.ToActionResult();
|
||||
step.ExecutionContext.SetGitHubContext("action_status", (ExecutionContext.Root.Result?.ToActionResult() ?? ActionResult.Cancelled).ToString().ToLowerInvariant());
|
||||
|
||||
step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'.");
|
||||
var conditionReTestTraceWriter = new ConditionTraceWriter(Trace, null); // host tracing only
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -56,7 +56,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
{
|
||||
Data.Image = Data.Image.Substring("docker://".Length);
|
||||
}
|
||||
else if (DockerUtil.IsDockerfile(Data.Image))
|
||||
else if (Data.Image.EndsWith("Dockerfile") || Data.Image.EndsWith("dockerfile"))
|
||||
{
|
||||
// ensure docker file exist
|
||||
dockerFile = Path.Combine(ActionDirectory, Data.Image);
|
||||
|
||||
@@ -68,28 +68,6 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
bool isOptOut = isWorkflowOptOutSet ? StringUtil.ConvertToBoolean(workflowOptOut) : isLocalOptOut;
|
||||
if (!isOptOut)
|
||||
{
|
||||
var repoAction = action as Pipelines.RepositoryPathReference;
|
||||
if (repoAction != null)
|
||||
{
|
||||
var warningActions = new HashSet<string>();
|
||||
if (executionContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode12DetectedAfterEndOfLifeEnvVariable, out var node16ForceWarnings))
|
||||
{
|
||||
warningActions = StringUtil.ConvertFromJson<HashSet<string>>(node16ForceWarnings);
|
||||
}
|
||||
|
||||
var repoActionFullName = "";
|
||||
if (string.IsNullOrEmpty(repoAction.Name))
|
||||
{
|
||||
repoActionFullName = repoAction.Path; // local actions don't have a 'Name'
|
||||
}
|
||||
else
|
||||
{
|
||||
repoActionFullName = $"{repoAction.Name}/{repoAction.Path ?? string.Empty}".TrimEnd('/') + $"@{repoAction.Ref}";
|
||||
}
|
||||
|
||||
warningActions.Add(repoActionFullName);
|
||||
executionContext.Global.Variables.Set("Node16ForceActionsWarnings", StringUtil.ConvertToJson(warningActions));
|
||||
}
|
||||
nodeData.NodeVersion = "node16";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -16,7 +15,6 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
@@ -36,13 +34,12 @@ namespace GitHub.Runner.Worker
|
||||
public interface IJobExtension : IRunnerService
|
||||
{
|
||||
Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message);
|
||||
Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc);
|
||||
void FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc);
|
||||
}
|
||||
|
||||
public sealed class JobExtension : RunnerService, IJobExtension
|
||||
{
|
||||
private readonly HashSet<string> _existingProcesses = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<Task<string>> _connectivityCheckTasks = new();
|
||||
private bool _processCleanup;
|
||||
private string _processLookupId = $"github_{Guid.NewGuid()}";
|
||||
private CancellationTokenSource _diskSpaceCheckToken = new();
|
||||
@@ -178,14 +175,7 @@ namespace GitHub.Runner.Worker
|
||||
context.Debug("Update context data");
|
||||
string _workDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
|
||||
context.SetRunnerContext("workspace", Path.Combine(_workDirectory, trackingConfig.PipelineDirectory));
|
||||
|
||||
var githubWorkspace = Path.Combine(_workDirectory, trackingConfig.WorkspaceDirectory);
|
||||
if (jobContext.Global.Variables.GetBoolean(Constants.Runner.Features.UseContainerPathForTemplate) ?? false)
|
||||
{
|
||||
// This value is used to translate paths from the container path back to the host path.
|
||||
context.SetGitHubContext("host-workspace", githubWorkspace);
|
||||
}
|
||||
context.SetGitHubContext("workspace", githubWorkspace);
|
||||
context.SetGitHubContext("workspace", Path.Combine(_workDirectory, trackingConfig.WorkspaceDirectory));
|
||||
|
||||
// Temporary hack for GHES alpha
|
||||
var configurationStore = HostContext.GetService<IConfigurationStore>();
|
||||
@@ -431,22 +421,6 @@ namespace GitHub.Runner.Worker
|
||||
_diskSpaceCheckTask = CheckDiskSpaceAsync(context, _diskSpaceCheckToken.Token);
|
||||
}
|
||||
|
||||
// Check server connectivity in background
|
||||
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
if (systemConnection.Data.TryGetValue("ConnectivityChecks", out var connectivityChecksPayload) &&
|
||||
!string.IsNullOrEmpty(connectivityChecksPayload))
|
||||
{
|
||||
Trace.Info($"Start checking server connectivity.");
|
||||
var checkUrls = StringUtil.ConvertFromJson<List<string>>(connectivityChecksPayload);
|
||||
if (checkUrls?.Count > 0)
|
||||
{
|
||||
foreach (var checkUrl in checkUrls)
|
||||
{
|
||||
_connectivityCheckTasks.Add(CheckConnectivity(checkUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
|
||||
@@ -491,7 +465,7 @@ namespace GitHub.Runner.Worker
|
||||
return reference;
|
||||
}
|
||||
|
||||
public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc)
|
||||
public void FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc)
|
||||
{
|
||||
Trace.Entering();
|
||||
ArgUtil.NotNull(jobContext, nameof(jobContext));
|
||||
@@ -668,28 +642,6 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
_diskSpaceCheckToken.Cancel();
|
||||
}
|
||||
|
||||
// Collect server connectivity check result
|
||||
if (_connectivityCheckTasks.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.Info($"Wait for all connectivity checks to finish.");
|
||||
await Task.WhenAll(_connectivityCheckTasks);
|
||||
foreach (var check in _connectivityCheckTasks)
|
||||
{
|
||||
var result = await check;
|
||||
Trace.Info($"Connectivity check result: {result}");
|
||||
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = result });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error($"Fail to check server connectivity.");
|
||||
Trace.Error(ex);
|
||||
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"Fail to check server connectivity. {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -705,37 +657,6 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> CheckConnectivity(string endpointUrl)
|
||||
{
|
||||
Trace.Info($"Check server connectivity for {endpointUrl}.");
|
||||
string result = string.Empty;
|
||||
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
|
||||
using (var httpClient = new HttpClient(httpClientHandler))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
|
||||
var response = await httpClient.GetAsync(endpointUrl, timeoutTokenSource.Token);
|
||||
result = $"{endpointUrl}: {response.StatusCode}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is OperationCanceledException && timeoutTokenSource.IsCancellationRequested)
|
||||
{
|
||||
Trace.Error($"Request timeout during connectivity check: {ex}");
|
||||
result = $"{endpointUrl}: timeout";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error($"Catch exception during connectivity check: {ex}");
|
||||
result = $"{endpointUrl}: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task CheckDiskSpaceAsync(IExecutionContext context, CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
@@ -229,7 +228,7 @@ namespace GitHub.Runner.Worker
|
||||
finally
|
||||
{
|
||||
Trace.Info("Finalize job.");
|
||||
await jobExtension.FinalizeJob(jobContext, message, jobStartTimeUtc);
|
||||
jobExtension.FinalizeJob(jobContext, message, jobStartTimeUtc);
|
||||
}
|
||||
|
||||
Trace.Info($"Job result after all job steps finish: {jobContext.Result ?? TaskResult.Succeeded}");
|
||||
@@ -284,13 +283,6 @@ namespace GitHub.Runner.Worker
|
||||
// Make sure we don't submit secrets as telemetry
|
||||
MaskTelemetrySecrets(jobContext.Global.JobTelemetry);
|
||||
|
||||
// Get environment url
|
||||
string environmentUrl = null;
|
||||
if (jobContext.ActionsEnvironment?.Url is StringToken urlStringToken)
|
||||
{
|
||||
environmentUrl = urlStringToken.Value;
|
||||
}
|
||||
|
||||
Trace.Info($"Raising job completed against run service");
|
||||
var completeJobRetryLimit = 5;
|
||||
var exceptions = new List<Exception>();
|
||||
@@ -298,7 +290,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
try
|
||||
{
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, default);
|
||||
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, default);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -374,12 +366,6 @@ namespace GitHub.Runner.Worker
|
||||
jobContext.Warning(string.Format(Constants.Runner.Node12DetectedAfterEndOfLife, actions));
|
||||
}
|
||||
|
||||
if (jobContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode12DetectedAfterEndOfLifeEnvVariable, out var node16ForceWarnings))
|
||||
{
|
||||
var actions = string.Join(", ", StringUtil.ConvertFromJson<HashSet<string>>(node16ForceWarnings));
|
||||
jobContext.Warning(string.Format(Constants.Runner.EnforcedNode12DetectedAfterEndOfLife, actions));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ShutdownQueue(throwOnFailure: true);
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace GitHub.Services.Common
|
||||
public class VssHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with default credentials and request
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with default credentials and request
|
||||
/// settings.
|
||||
/// </summary>
|
||||
public VssHttpMessageHandler()
|
||||
@@ -25,7 +25,7 @@ namespace GitHub.Services.Common
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
||||
/// settings.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials which should be used</param>
|
||||
@@ -38,7 +38,7 @@ namespace GitHub.Services.Common
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
||||
/// Initializes a new <c>VssHttpMessageHandler</c> instance with the specified credentials and request
|
||||
/// settings.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials which should be used</param>
|
||||
@@ -211,33 +211,14 @@ namespace GitHub.Services.Common
|
||||
|
||||
traceInfo?.TraceBufferedRequestTime();
|
||||
|
||||
// ConfigureAwait(false) enables the continuation to be run outside any captured
|
||||
// ConfigureAwait(false) enables the continuation to be run outside any captured
|
||||
// SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking...
|
||||
|
||||
var tmpResponse = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
|
||||
if (Settings.AllowAutoRedirectForBroker && tmpResponse.StatusCode == HttpStatusCode.Redirect)
|
||||
{
|
||||
//Dispose of the previous response
|
||||
tmpResponse?.Dispose();
|
||||
|
||||
var location = tmpResponse.Headers.Location;
|
||||
request = new HttpRequestMessage(HttpMethod.Get, location);
|
||||
|
||||
// Reapply the token to new redirected request
|
||||
ApplyToken(request, token, applyICredentialsToWebProxy: lastResponseDemandedProxyAuth);
|
||||
|
||||
// Resend the request
|
||||
response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = tmpResponse;
|
||||
}
|
||||
response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
traceInfo?.TraceRequestSendTime();
|
||||
|
||||
// Now buffer the response content if configured to do so. In general we will be buffering
|
||||
// the response content in this location, except in the few cases where the caller has
|
||||
// the response content in this location, except in the few cases where the caller has
|
||||
// specified HttpCompletionOption.ResponseHeadersRead.
|
||||
// Trace content type in case of error
|
||||
await BufferResponseContentAsync(request, response, () => $"[ContentType: {response.Content.GetType().Name}]", tokenSource.Token).ConfigureAwait(false);
|
||||
@@ -254,7 +235,7 @@ namespace GitHub.Services.Common
|
||||
provider.ValidateToken(token, responseWrapper);
|
||||
}
|
||||
|
||||
// Make sure that once we can authenticate with the service that we turn off the
|
||||
// Make sure that once we can authenticate with the service that we turn off the
|
||||
// Expect100Continue behavior to increase performance.
|
||||
this.ExpectContinue = false;
|
||||
succeeded = true;
|
||||
@@ -300,8 +281,8 @@ namespace GitHub.Services.Common
|
||||
}
|
||||
|
||||
// If the user has already tried once but still unauthorized, stop retrying. The main scenario for this condition
|
||||
// is a user typed in a valid credentials for a hosted account but the associated identity does not have
|
||||
// access. We do not want to continually prompt 3 times without telling them the failure reason. In the
|
||||
// is a user typed in a valid credentials for a hosted account but the associated identity does not have
|
||||
// access. We do not want to continually prompt 3 times without telling them the failure reason. In the
|
||||
// next release we should rethink about presenting user the failure and options between retries.
|
||||
IEnumerable<String> headerValues;
|
||||
Boolean hasAuthenticateError =
|
||||
@@ -508,7 +489,7 @@ namespace GitHub.Services.Common
|
||||
httpClientHandler.AllowAutoRedirect = settings.AllowAutoRedirect;
|
||||
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
||||
//Setting httpClientHandler.UseDefaultCredentials to false in .Net Core, clears httpClientHandler.Credentials if
|
||||
//credentials is already set to defaultcredentials. Therefore httpClientHandler.Credentials must be
|
||||
//credentials is already set to defaultcredentials. Therefore httpClientHandler.Credentials must be
|
||||
//set after httpClientHandler.UseDefaultCredentials.
|
||||
httpClientHandler.UseDefaultCredentials = false;
|
||||
httpClientHandler.Credentials = defaultCredentials;
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace GitHub.Services.Common
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not HttpClientHandler should follow redirect on outgoing requests.
|
||||
/// Gets or sets a value indicating whether or not HttpClientHandler should follow redirect on outgoing requests.
|
||||
/// </summary>
|
||||
public Boolean AllowAutoRedirect
|
||||
{
|
||||
@@ -111,17 +111,7 @@ namespace GitHub.Services.Common
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not HttpClientHandler should follow redirect on outgoing broker requests
|
||||
/// This is special since this also sends token in the request, where as default AllowAutoRedirect does not
|
||||
/// </summary>
|
||||
public Boolean AllowAutoRedirectForBroker
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not compression should be used on outgoing requests.
|
||||
/// Gets or sets a value indicating whether or not compression should be used on outgoing requests.
|
||||
/// The default value is true.
|
||||
/// </summary>
|
||||
[DefaultValue(true)]
|
||||
|
||||
@@ -82,6 +82,5 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
|
||||
public const String WorkflowRoot = "workflow-root";
|
||||
public const String WorkingDirectory = "working-directory";
|
||||
public const String Workspace = "workspace";
|
||||
public const String HostWorkspace = "host-workspace";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,5 @@ namespace GitHub.DistributedTask.WebApi
|
||||
|
||||
[EnumMember]
|
||||
ActionCommand = 1,
|
||||
|
||||
[EnumMember]
|
||||
ConnectivityCheck = 2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,5 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
|
||||
[DataMember(Name = "annotations", EmitDefaultValue = false)]
|
||||
public IList<Annotation> Annotations { get; set; }
|
||||
|
||||
[DataMember(Name = "environmentUrl", EmitDefaultValue = false)]
|
||||
public string EnvironmentUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
Dictionary<String, VariableValue> outputs,
|
||||
IList<StepResult> stepResults,
|
||||
IList<Annotation> jobAnnotations,
|
||||
string environmentUrl,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpMethod httpMethod = new HttpMethod("POST");
|
||||
@@ -111,8 +110,7 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
Conclusion = result,
|
||||
Outputs = outputs,
|
||||
StepResults = stepResults,
|
||||
Annotations = jobAnnotations,
|
||||
EnvironmentUrl = environmentUrl,
|
||||
Annotations = jobAnnotations
|
||||
};
|
||||
|
||||
requestUri = new Uri(requestUri, "completejob");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using Xunit;
|
||||
@@ -145,49 +145,6 @@ namespace GitHub.Runner.Common.Tests.Worker.Container
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
[InlineData("dockerhub/repo", false)]
|
||||
[InlineData("debian:latest", false)]
|
||||
[InlineData("something/dockerfileimage", false)]
|
||||
[InlineData("ghcr.io/docker/dockerfile", true)] // should be false but might break the current workflows
|
||||
[InlineData("Dockerfile", true)]
|
||||
[InlineData("Dockerfile.", true)]
|
||||
[InlineData(".Dockerfile", true)]
|
||||
[InlineData(".Dockerfile.", false)]
|
||||
[InlineData("dockerfile", true)]
|
||||
[InlineData("dockerfile.", true)]
|
||||
[InlineData(".dockerfile", true)]
|
||||
[InlineData(".dockerfile.", false)]
|
||||
[InlineData("Dockerfile.test", true)]
|
||||
[InlineData("test.Dockerfile", true)]
|
||||
[InlineData("docker/dockerfile:latest", false)]
|
||||
[InlineData("/some/path/dockerfile:latest", false)]
|
||||
[InlineData("dockerfile:latest", false)]
|
||||
[InlineData("Dockerfile:latest", false)]
|
||||
[InlineData("dockerfile-latest", false)]
|
||||
[InlineData("Dockerfile-latest", false)]
|
||||
[InlineData("dockerfile.latest", true)]
|
||||
[InlineData("Dockerfile.latest", true)]
|
||||
[InlineData("../dockerfile/dockerfileone", false)]
|
||||
[InlineData("../Dockerfile/dockerfileone", false)]
|
||||
[InlineData("../dockerfile.test", true)]
|
||||
[InlineData("../Dockerfile.test", true)]
|
||||
[InlineData("./dockerfile/image", false)]
|
||||
[InlineData("./Dockerfile/image", false)]
|
||||
[InlineData("example/Dockerfile.test", true)]
|
||||
[InlineData("./example/Dockerfile.test", true)]
|
||||
[InlineData("example/test.dockerfile", true)]
|
||||
[InlineData("./example/test.dockerfile", true)]
|
||||
[InlineData("docker://Dockerfile", false)]
|
||||
[InlineData("docker://ubuntu:latest", false)]
|
||||
public void IsDockerfile(string input, bool expected)
|
||||
{
|
||||
var actual = DockerUtil.IsDockerfile(input);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
|
||||
@@ -186,8 +186,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
{
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("http_proxy", "#fragment");
|
||||
Environment.SetEnvironmentVariable("https_proxy", "#fragment");
|
||||
Environment.SetEnvironmentVariable("http_proxy", "127.0.0.1:7777");
|
||||
Environment.SetEnvironmentVariable("https_proxy", "127.0.0.1");
|
||||
var proxy = new RunnerWebProxy();
|
||||
|
||||
Assert.Null(proxy.HttpProxyAddress);
|
||||
@@ -206,68 +206,6 @@ namespace GitHub.Runner.Common.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void WebProxyPrependsHTTPforHTTP_PROXY_IfNoProtocol()
|
||||
{
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("http_proxy", "127.0.0.1:7777");
|
||||
var proxy = new RunnerWebProxy();
|
||||
|
||||
Assert.Equal("http://127.0.0.1:7777", proxy.HttpProxyAddress);
|
||||
Assert.Null(proxy.HttpProxyUsername);
|
||||
Assert.Null(proxy.HttpProxyPassword);
|
||||
|
||||
Assert.Equal(0, proxy.NoProxyList.Count);
|
||||
|
||||
Environment.SetEnvironmentVariable("http_proxy", "http://127.0.0.1:7777");
|
||||
proxy = new RunnerWebProxy();
|
||||
|
||||
Assert.Equal("http://127.0.0.1:7777", proxy.HttpProxyAddress);
|
||||
Assert.Null(proxy.HttpProxyUsername);
|
||||
Assert.Null(proxy.HttpProxyPassword);
|
||||
|
||||
Assert.Equal(0, proxy.NoProxyList.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanProxyEnv();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
public void WebProxyPrependsHTTPforHTTPS_PROXY_IfNoProtocol()
|
||||
{
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("https_proxy", "127.0.0.1:7777");
|
||||
var proxy = new RunnerWebProxy();
|
||||
|
||||
Assert.Equal("http://127.0.0.1:7777", proxy.HttpsProxyAddress);
|
||||
Assert.Null(proxy.HttpProxyUsername);
|
||||
Assert.Null(proxy.HttpProxyPassword);
|
||||
|
||||
Assert.Equal(0, proxy.NoProxyList.Count);
|
||||
|
||||
Environment.SetEnvironmentVariable("https_proxy", "https://127.0.0.1:7777");
|
||||
proxy = new RunnerWebProxy();
|
||||
|
||||
Assert.Equal("https://127.0.0.1:7777", proxy.HttpsProxyAddress); // existing protocol 'https' is not removed
|
||||
Assert.Null(proxy.HttpProxyUsername);
|
||||
Assert.Null(proxy.HttpProxyPassword);
|
||||
|
||||
Assert.Equal(0, proxy.NoProxyList.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CleanProxyEnv();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Common")]
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests
|
||||
{
|
||||
public enum LineEndingType
|
||||
{
|
||||
Native,
|
||||
Linux = 0x__0A,
|
||||
Windows = 0x0D0A
|
||||
}
|
||||
|
||||
public static class TestUtil
|
||||
{
|
||||
private const string Src = "src";
|
||||
@@ -52,24 +41,5 @@ namespace GitHub.Runner.Common.Tests
|
||||
Assert.True(Directory.Exists(testDataDir));
|
||||
return testDataDir;
|
||||
}
|
||||
|
||||
public static void WriteContent(string path, string content, LineEndingType lineEnding = LineEndingType.Native)
|
||||
{
|
||||
WriteContent(path, Enumerable.Repeat(content, 1), lineEnding);
|
||||
}
|
||||
|
||||
public static void WriteContent(string path, IEnumerable<string> content, LineEndingType lineEnding = LineEndingType.Native)
|
||||
{
|
||||
string newline = lineEnding switch
|
||||
{
|
||||
LineEndingType.Linux => "\n",
|
||||
LineEndingType.Windows => "\r\n",
|
||||
_ => Environment.NewLine,
|
||||
};
|
||||
var encoding = new UTF8Encoding(true); // Emit BOM
|
||||
var contentStr = string.Join(newline, content);
|
||||
File.WriteAllText(path, contentStr, encoding);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
"",
|
||||
"## This is more markdown content",
|
||||
};
|
||||
TestUtil.WriteContent(stepSummaryFile, content);
|
||||
WriteContent(stepSummaryFile, content);
|
||||
|
||||
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||
_jobExecutionContext.Complete();
|
||||
@@ -153,7 +153,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
"",
|
||||
"# GITHUB_TOKEN ghs_verysecuretoken",
|
||||
};
|
||||
TestUtil.WriteContent(stepSummaryFile, content);
|
||||
WriteContent(stepSummaryFile, content);
|
||||
|
||||
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
|
||||
|
||||
@@ -167,6 +167,21 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteContent(
|
||||
string path,
|
||||
List<string> content,
|
||||
string newline = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newline))
|
||||
{
|
||||
newline = Environment.NewLine;
|
||||
}
|
||||
|
||||
var encoding = new UTF8Encoding(true); // Emit BOM
|
||||
var contentStr = string.Join(newline, content);
|
||||
File.WriteAllText(path, contentStr, encoding);
|
||||
}
|
||||
|
||||
private TestHostContext Setup([CallerMemberName] string name = "")
|
||||
{
|
||||
var hostContext = new TestHostContext(this, name);
|
||||
|
||||
@@ -821,7 +821,8 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
ec.Complete();
|
||||
|
||||
// Assert.
|
||||
Assert.Equal(0, ec.Global.StepsResult.Count);
|
||||
Assert.Equal(1, ec.Global.StepsResult.Count);
|
||||
Assert.Equal(TaskResult.Succeeded, ec.Global.StepsResult.Single().Conclusion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,420 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using DTWebApi = GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public abstract class FileCommandTestBase<T>
|
||||
where T : IFileCommandExtension, new()
|
||||
{
|
||||
|
||||
protected void TestDirectoryNotFound()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _store.Count);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestNotFound()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _store.Count);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestEmptyFile()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "empty-file");
|
||||
var content = new List<string>();
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _store.Count);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestSimple()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY=MY VALUE",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _store.Count);
|
||||
Assert.Equal("MY VALUE", _store["MY_KEY"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestSimple_SkipEmptyLines()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_KEY=my value",
|
||||
string.Empty,
|
||||
"MY_KEY_2=my second value",
|
||||
string.Empty,
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _store.Count);
|
||||
Assert.Equal("my value", _store["MY_KEY"]);
|
||||
Assert.Equal("my second value", _store["MY_KEY_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestSimple_EmptyValue()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple-empty-value");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY=",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _store.Count);
|
||||
Assert.Equal(string.Empty, _store["MY_KEY"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestSimple_MultipleValues()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY=my value",
|
||||
"MY_KEY_2=",
|
||||
"MY_KEY_3=my third value",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _store.Count);
|
||||
Assert.Equal("my value", _store["MY_KEY"]);
|
||||
Assert.Equal(string.Empty, _store["MY_KEY_2"]);
|
||||
Assert.Equal("my third value", _store["MY_KEY_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestSimple_SpecialCharacters()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY==abc",
|
||||
"MY_KEY_2=def=ghi",
|
||||
"MY_KEY_3=jkl=",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _store.Count);
|
||||
Assert.Equal("=abc", _store["MY_KEY"]);
|
||||
Assert.Equal("def=ghi", _store["MY_KEY_2"]);
|
||||
Assert.Equal("jkl=", _store["MY_KEY_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY<<EOF",
|
||||
"line one",
|
||||
"line two",
|
||||
"line three",
|
||||
"EOF",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _store.Count);
|
||||
Assert.Equal($"line one{BREAK}line two{BREAK}line three", _store["MY_KEY"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc_EmptyValue()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY<<EOF",
|
||||
"EOF",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _store.Count);
|
||||
Assert.Equal(string.Empty, _store["MY_KEY"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc_SkipEmptyLines()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_KEY<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
"MY_KEY_2<<EOF",
|
||||
"HELLO",
|
||||
"AGAIN",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _store.Count);
|
||||
Assert.Equal($"hello{BREAK}world", _store["MY_KEY"]);
|
||||
Assert.Equal($"HELLO{BREAK}AGAIN", _store["MY_KEY_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc_EdgeCases()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY_1<<EOF",
|
||||
"hello",
|
||||
string.Empty,
|
||||
"three",
|
||||
string.Empty,
|
||||
"EOF",
|
||||
"MY_KEY_2<<EOF",
|
||||
"hello=two",
|
||||
"EOF",
|
||||
"MY_KEY_3<<EOF",
|
||||
" EOF",
|
||||
"EOF",
|
||||
"MY_KEY_4<<EOF",
|
||||
"EOF EOF",
|
||||
"EOF",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(4, _store.Count);
|
||||
Assert.Equal($"hello{BREAK}{BREAK}three{BREAK}", _store["MY_KEY_1"]);
|
||||
Assert.Equal($"hello=two", _store["MY_KEY_2"]);
|
||||
Assert.Equal($" EOF", _store["MY_KEY_3"]);
|
||||
Assert.Equal($"EOF EOF", _store["MY_KEY_4"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc_EndMarkerVariations(string validEndMarker)
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
string eof = validEndMarker;
|
||||
var content = new List<string>
|
||||
{
|
||||
$"MY_KEY_1<<{eof}",
|
||||
$"hello",
|
||||
$"one",
|
||||
$"{eof}",
|
||||
$"MY_KEY_2<<{eof}",
|
||||
$"hello=two",
|
||||
$"{eof}",
|
||||
$"MY_KEY_3<<{eof}",
|
||||
$" {eof}",
|
||||
$"{eof}",
|
||||
$"MY_KEY_4<<{eof}",
|
||||
$"{eof} {eof}",
|
||||
$"{eof}",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(4, _store.Count);
|
||||
Assert.Equal($"hello{BREAK}one", _store["MY_KEY_1"]);
|
||||
Assert.Equal($"hello=two", _store["MY_KEY_2"]);
|
||||
Assert.Equal($" {eof}", _store["MY_KEY_3"]);
|
||||
Assert.Equal($"{eof} {eof}", _store["MY_KEY_4"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc_EqualBeforeMultilineIndicator()
|
||||
{
|
||||
using var hostContext = Setup();
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
|
||||
// Define a hypothetical injectable payload that just happens to contain the '=' character.
|
||||
string contrivedGitHubIssueTitle = "Issue 999: Better handling for the `=` character";
|
||||
|
||||
// The docs recommend using randomly-generated EOF markers.
|
||||
// Here's a randomly-generated base64 EOF marker that just happens to contain an '=' character. ('=' is a padding character in base64.)
|
||||
// see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
string randomizedEOF = "khkIhPxsVA==";
|
||||
var content = new List<string>
|
||||
{
|
||||
// In a real world scenario, "%INJECT%" might instead be something like "${{ github.event.issue.title }}"
|
||||
$"PREFIX_%INJECT%<<{randomizedEOF}".Replace("%INJECT%", contrivedGitHubIssueTitle),
|
||||
"RandomDataThatJustHappensToContainAnEquals=Character",
|
||||
randomizedEOF,
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
var ex = Assert.Throws<Exception>(() => _fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null));
|
||||
Assert.StartsWith("Invalid format", ex.Message);
|
||||
}
|
||||
|
||||
protected void TestHeredoc_MissingNewLine()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
string content = "MY_KEY<<EOF line one line two line three EOF";
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
var ex = Assert.Throws<Exception>(() => _fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null));
|
||||
Assert.Contains("Matching delimiter not found", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc_MissingNewLineMultipleLines()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
string multilineFragment = @"line one
|
||||
line two
|
||||
line three";
|
||||
|
||||
// Note that the final EOF does not appear on it's own line.
|
||||
string content = $"MY_KEY<<EOF {multilineFragment} EOF";
|
||||
TestUtil.WriteContent(stateFile, content);
|
||||
var ex = Assert.Throws<Exception>(() => _fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null));
|
||||
Assert.Contains("EOF marker missing new line", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected void TestHeredoc_PreservesNewline()
|
||||
{
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var newline = "\n";
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_KEY<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
};
|
||||
TestUtil.WriteContent(stateFile, content, LineEndingType.Linux);
|
||||
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _store.Count);
|
||||
Assert.Equal($"hello{newline}world", _store["MY_KEY"]);
|
||||
}
|
||||
}
|
||||
|
||||
protected TestHostContext Setup([CallerMemberName] string name = "")
|
||||
{
|
||||
_issues = new List<Tuple<DTWebApi.Issue, string>>();
|
||||
|
||||
var hostContext = new TestHostContext(this, name);
|
||||
|
||||
// Trace
|
||||
_trace = hostContext.GetTrace();
|
||||
|
||||
// Directory for test data
|
||||
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
|
||||
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
|
||||
Directory.CreateDirectory(workDirectory);
|
||||
_rootDirectory = Path.Combine(workDirectory, nameof(T));
|
||||
Directory.CreateDirectory(_rootDirectory);
|
||||
|
||||
// Execution context
|
||||
_executionContext = new Mock<IExecutionContext>();
|
||||
_executionContext.Setup(x => x.Global)
|
||||
.Returns(new GlobalContext
|
||||
{
|
||||
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
|
||||
WriteDebug = true,
|
||||
});
|
||||
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
|
||||
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
|
||||
{
|
||||
var resolvedMessage = issue.Message;
|
||||
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
|
||||
{
|
||||
resolvedMessage = logOptions.LogMessageOverride;
|
||||
}
|
||||
_issues.Add(new(issue, resolvedMessage));
|
||||
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
|
||||
});
|
||||
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((string tag, string message) =>
|
||||
{
|
||||
_trace.Info($"{tag}{message}");
|
||||
});
|
||||
|
||||
_store = PostSetup();
|
||||
|
||||
_fileCmdExtension = new T();
|
||||
_fileCmdExtension.Initialize(hostContext);
|
||||
|
||||
return hostContext;
|
||||
}
|
||||
|
||||
protected abstract IDictionary<string, string> PostSetup();
|
||||
|
||||
protected static readonly string BREAK = Environment.NewLine;
|
||||
|
||||
protected IFileCommandExtension _fileCmdExtension { get; private set; }
|
||||
protected Mock<IExecutionContext> _executionContext { get; private set; }
|
||||
protected List<Tuple<DTWebApi.Issue, string>> _issues { get; private set; }
|
||||
protected IDictionary<string, string> _store { get; private set; }
|
||||
protected string _rootDirectory { get; private set; }
|
||||
protected ITraceWriter _trace { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Worker;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Worker;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using System.Threading;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
@@ -105,18 +105,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
github["repository"] = new Pipelines.ContextData.StringContextData("actions/runner");
|
||||
github["secret_source"] = new Pipelines.ContextData.StringContextData("Actions");
|
||||
_message.ContextData.Add("github", github);
|
||||
_message.Resources.Endpoints.Add(new ServiceEndpoint()
|
||||
{
|
||||
Name = WellKnownServiceEndpointNames.SystemVssConnection,
|
||||
Url = new Uri("https://pipelines.actions.githubusercontent.com"),
|
||||
Authorization = new EndpointAuthorization()
|
||||
{
|
||||
Scheme = "Test",
|
||||
Parameters = {
|
||||
{"AccessToken", "token"}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
hc.SetSingleton(_actionManager.Object);
|
||||
hc.SetSingleton(_config.Object);
|
||||
@@ -243,7 +231,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task UploadDiganosticLogIfEnvironmentVariableSet()
|
||||
public void UploadDiganosticLogIfEnvironmentVariableSet()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
@@ -256,7 +244,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
_jobEc.Initialize(hc);
|
||||
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
_diagnosticLogManager.Verify(x =>
|
||||
x.UploadDiagnosticLogs(
|
||||
@@ -271,7 +259,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task DontUploadDiagnosticLogIfEnvironmentVariableFalse()
|
||||
public void DontUploadDiagnosticLogIfEnvironmentVariableFalse()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
@@ -284,7 +272,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
_jobEc.Initialize(hc);
|
||||
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
_diagnosticLogManager.Verify(x =>
|
||||
x.UploadDiagnosticLogs(
|
||||
@@ -299,14 +287,14 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task DontUploadDiagnosticLogIfEnvironmentVariableMissing()
|
||||
public void DontUploadDiagnosticLogIfEnvironmentVariableMissing()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
var jobExtension = new JobExtension();
|
||||
jobExtension.Initialize(hc);
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
_diagnosticLogManager.Verify(x =>
|
||||
x.UploadDiagnosticLogs(
|
||||
@@ -321,7 +309,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task EnsureFinalizeJobRunsIfMessageHasNoEnvironmentUrl()
|
||||
public void EnsureFinalizeJobRunsIfMessageHasNoEnvironmentUrl()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
@@ -334,7 +322,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
_jobEc.Initialize(hc);
|
||||
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
|
||||
}
|
||||
@@ -343,7 +331,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task EnsureFinalizeJobHandlesNullEnvironmentUrl()
|
||||
public void EnsureFinalizeJobHandlesNullEnvironmentUrl()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
@@ -359,7 +347,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
_jobEc.Initialize(hc);
|
||||
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
|
||||
}
|
||||
@@ -368,7 +356,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task EnsureFinalizeJobHandlesNullEnvironment()
|
||||
public void EnsureFinalizeJobHandlesNullEnvironment()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
@@ -381,7 +369,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
_jobEc.Initialize(hc);
|
||||
_jobEc.InitializeJob(_message, _tokenSource.Token);
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
|
||||
}
|
||||
@@ -409,7 +397,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
|
||||
var hookStart = result.First() as JobExtensionRunner;
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
Assert.Equal(Constants.Hooks.JobStartedStepName, hookStart.DisplayName);
|
||||
Assert.Equal(Constants.Hooks.JobCompletedStepName, (_jobEc.PostJobSteps.Last() as JobExtensionRunner).DisplayName);
|
||||
@@ -422,7 +410,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task EnsureNoPreAndPostHookSteps()
|
||||
public void EnsureNoPreAndPostHookSteps()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
@@ -437,7 +425,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
|
||||
var x = _jobEc.JobSteps;
|
||||
|
||||
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
|
||||
|
||||
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
|
||||
Assert.Equal(0, _jobEc.PostJobSteps.Count);
|
||||
|
||||
@@ -99,8 +99,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
timeline,
|
||||
jobId,
|
||||
testName,
|
||||
testName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null,
|
||||
new ActionsEnvironmentReference("staging"),
|
||||
testName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null,
|
||||
messageType: messageType);
|
||||
message.Variables[Constants.Variables.System.Culture] = "en-US";
|
||||
message.Resources.Endpoints.Add(new ServiceEndpoint()
|
||||
|
||||
@@ -1,27 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using DTWebApi = GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class SaveStateFileCommandL0 : FileCommandTestBase<SaveStateFileCommand>
|
||||
public sealed class SaveStateFileCommandL0
|
||||
{
|
||||
|
||||
protected override IDictionary<string, string> PostSetup()
|
||||
{
|
||||
var intraActionState = new Dictionary<string, string>();
|
||||
_executionContext.Setup(x => x.IntraActionState).Returns(intraActionState);
|
||||
return intraActionState;
|
||||
}
|
||||
private Mock<IExecutionContext> _executionContext;
|
||||
private List<Tuple<DTWebApi.Issue, string>> _issues;
|
||||
private string _rootDirectory;
|
||||
private SaveStateFileCommand _saveStateFileCommand;
|
||||
private Dictionary<string, string> _intraActionState;
|
||||
private ITraceWriter _trace;
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_DirectoryNotFound()
|
||||
{
|
||||
base.TestDirectoryNotFound();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _intraActionState.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -29,7 +46,13 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_NotFound()
|
||||
{
|
||||
base.TestNotFound();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _intraActionState.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -37,7 +60,15 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_EmptyFile()
|
||||
{
|
||||
base.TestEmptyFile();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "empty-file");
|
||||
var content = new List<string>();
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _intraActionState.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -45,7 +76,19 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Simple()
|
||||
{
|
||||
base.TestSimple();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE=MY VALUE",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _intraActionState.Count);
|
||||
Assert.Equal("MY VALUE", _intraActionState["MY_STATE"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -53,7 +96,24 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Simple_SkipEmptyLines()
|
||||
{
|
||||
base.TestSimple_SkipEmptyLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_STATE=my value",
|
||||
string.Empty,
|
||||
"MY_STATE_2=my second value",
|
||||
string.Empty,
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _intraActionState.Count);
|
||||
Assert.Equal("my value", _intraActionState["MY_STATE"]);
|
||||
Assert.Equal("my second value", _intraActionState["MY_STATE_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -61,7 +121,19 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Simple_EmptyValue()
|
||||
{
|
||||
base.TestSimple_EmptyValue();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple-empty-value");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE=",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _intraActionState.Count);
|
||||
Assert.Equal(string.Empty, _intraActionState["MY_STATE"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -69,7 +141,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Simple_MultipleValues()
|
||||
{
|
||||
base.TestSimple_MultipleValues();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE=my value",
|
||||
"MY_STATE_2=",
|
||||
"MY_STATE_3=my third value",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _intraActionState.Count);
|
||||
Assert.Equal("my value", _intraActionState["MY_STATE"]);
|
||||
Assert.Equal(string.Empty, _intraActionState["MY_STATE_2"]);
|
||||
Assert.Equal("my third value", _intraActionState["MY_STATE_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -77,7 +165,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Simple_SpecialCharacters()
|
||||
{
|
||||
base.TestSimple_SpecialCharacters();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE==abc",
|
||||
"MY_STATE_2=def=ghi",
|
||||
"MY_STATE_3=jkl=",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _intraActionState.Count);
|
||||
Assert.Equal("=abc", _intraActionState["MY_STATE"]);
|
||||
Assert.Equal("def=ghi", _intraActionState["MY_STATE_2"]);
|
||||
Assert.Equal("jkl=", _intraActionState["MY_STATE_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -85,7 +189,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc()
|
||||
{
|
||||
base.TestHeredoc();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE<<EOF",
|
||||
"line one",
|
||||
"line two",
|
||||
"line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _intraActionState.Count);
|
||||
Assert.Equal($"line one{Environment.NewLine}line two{Environment.NewLine}line three", _intraActionState["MY_STATE"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -93,7 +213,20 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_EmptyValue()
|
||||
{
|
||||
base.TestHeredoc_EmptyValue();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE<<EOF",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _intraActionState.Count);
|
||||
Assert.Equal(string.Empty, _intraActionState["MY_STATE"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -101,52 +234,73 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_SkipEmptyLines()
|
||||
{
|
||||
base.TestHeredoc_SkipEmptyLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_STATE<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
"MY_STATE_2<<EOF",
|
||||
"HELLO",
|
||||
"AGAIN",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _intraActionState.Count);
|
||||
Assert.Equal($"hello{Environment.NewLine}world", _intraActionState["MY_STATE"]);
|
||||
Assert.Equal($"HELLO{Environment.NewLine}AGAIN", _intraActionState["MY_STATE_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_EdgeCases()
|
||||
public void SaveStateFileCommand_Heredoc_SpecialCharacters()
|
||||
{
|
||||
base.TestHeredoc_EdgeCases();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
// All of the following are not only valid, but quite plausible end markers.
|
||||
// Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
#pragma warning disable format
|
||||
[InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
|
||||
[InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
|
||||
[InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
|
||||
[InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
|
||||
#pragma warning restore format
|
||||
[InlineData("<<//++==")]
|
||||
[InlineData("contrivedBase64==")]
|
||||
[InlineData("khkIhPxsVA==")]
|
||||
[InlineData("D+Y8zE/EOw==")]
|
||||
[InlineData("wuOWG4S6FQ==")]
|
||||
[InlineData("7wigCJ//iw==")]
|
||||
[InlineData("uifTuYTs8K4=")]
|
||||
[InlineData("M7N2ITg/04c=")]
|
||||
[InlineData("Xhh+qp+Y6iM=")]
|
||||
[InlineData("5tdblQajc/b+EGBZXo0w")]
|
||||
[InlineData("jk/UMjIx/N0eVcQYOUfw")]
|
||||
[InlineData("/n5lsw73Cwl35Hfuscdz")]
|
||||
[InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
|
||||
public void SaveStateFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
|
||||
{
|
||||
base.TestHeredoc_EndMarkerVariations(validEndMarker);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_EqualBeforeMultilineIndicator()
|
||||
{
|
||||
base.TestHeredoc_EqualBeforeMultilineIndicator();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE<<=EOF",
|
||||
"hello",
|
||||
"one",
|
||||
"=EOF",
|
||||
"MY_STATE_2<<<EOF",
|
||||
"hello",
|
||||
"two",
|
||||
"<EOF",
|
||||
"MY_STATE_3<<EOF",
|
||||
"hello",
|
||||
string.Empty,
|
||||
"three",
|
||||
string.Empty,
|
||||
"EOF",
|
||||
"MY_STATE_4<<EOF",
|
||||
"hello=four",
|
||||
"EOF",
|
||||
"MY_STATE_5<<EOF",
|
||||
" EOF",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(5, _intraActionState.Count);
|
||||
Assert.Equal($"hello{Environment.NewLine}one", _intraActionState["MY_STATE"]);
|
||||
Assert.Equal($"hello{Environment.NewLine}two", _intraActionState["MY_STATE_2"]);
|
||||
Assert.Equal($"hello{Environment.NewLine}{Environment.NewLine}three{Environment.NewLine}", _intraActionState["MY_STATE_3"]);
|
||||
Assert.Equal($"hello=four", _intraActionState["MY_STATE_4"]);
|
||||
Assert.Equal($" EOF", _intraActionState["MY_STATE_5"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -154,7 +308,21 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_MissingNewLine()
|
||||
{
|
||||
base.TestHeredoc_MissingNewLine();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE<<EOF",
|
||||
"line one",
|
||||
"line two",
|
||||
"line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content, " ");
|
||||
var ex = Assert.Throws<Exception>(() => _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
|
||||
Assert.Contains("Matching delimiter not found", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -162,7 +330,21 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_MissingNewLineMultipleLines()
|
||||
{
|
||||
base.TestHeredoc_MissingNewLineMultipleLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE<<EOF",
|
||||
@"line one
|
||||
line two
|
||||
line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content, " ");
|
||||
var ex = Assert.Throws<Exception>(() => _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
|
||||
Assert.Contains("EOF marker missing new line", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
@@ -171,9 +353,90 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_PreservesNewline()
|
||||
{
|
||||
base.TestHeredoc_PreservesNewline();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var newline = "\n";
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_STATE<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content, newline: newline);
|
||||
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _intraActionState.Count);
|
||||
Assert.Equal($"hello{newline}world", _intraActionState["MY_STATE"]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void WriteContent(
|
||||
string path,
|
||||
List<string> content,
|
||||
string newline = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newline))
|
||||
{
|
||||
newline = Environment.NewLine;
|
||||
}
|
||||
|
||||
var encoding = new UTF8Encoding(true); // Emit BOM
|
||||
var contentStr = string.Join(newline, content);
|
||||
File.WriteAllText(path, contentStr, encoding);
|
||||
}
|
||||
|
||||
private TestHostContext Setup([CallerMemberName] string name = "")
|
||||
{
|
||||
_issues = new List<Tuple<DTWebApi.Issue, string>>();
|
||||
_intraActionState = new Dictionary<string, string>();
|
||||
|
||||
var hostContext = new TestHostContext(this, name);
|
||||
|
||||
// Trace
|
||||
_trace = hostContext.GetTrace();
|
||||
|
||||
// Directory for test data
|
||||
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
|
||||
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
|
||||
Directory.CreateDirectory(workDirectory);
|
||||
_rootDirectory = Path.Combine(workDirectory, nameof(SaveStateFileCommandL0));
|
||||
Directory.CreateDirectory(_rootDirectory);
|
||||
|
||||
// Execution context
|
||||
_executionContext = new Mock<IExecutionContext>();
|
||||
_executionContext.Setup(x => x.Global)
|
||||
.Returns(new GlobalContext
|
||||
{
|
||||
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
|
||||
WriteDebug = true,
|
||||
});
|
||||
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
|
||||
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
|
||||
{
|
||||
var resolvedMessage = issue.Message;
|
||||
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
|
||||
{
|
||||
resolvedMessage = logOptions.LogMessageOverride;
|
||||
}
|
||||
_issues.Add(new(issue, resolvedMessage));
|
||||
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
|
||||
});
|
||||
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((string tag, string message) =>
|
||||
{
|
||||
_trace.Info($"{tag}{message}");
|
||||
});
|
||||
_executionContext.Setup(x => x.IntraActionState)
|
||||
.Returns(_intraActionState);
|
||||
|
||||
// SaveStateFileCommand
|
||||
_saveStateFileCommand = new SaveStateFileCommand();
|
||||
_saveStateFileCommand.Initialize(hostContext);
|
||||
|
||||
return hostContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using DTWebApi = GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class SetEnvFileCommandL0 : FileCommandTestBase<SetEnvFileCommand>
|
||||
public sealed class SetEnvFileCommandL0
|
||||
{
|
||||
|
||||
protected override IDictionary<string, string> PostSetup()
|
||||
{
|
||||
return _executionContext.Object.Global.EnvironmentVariables;
|
||||
}
|
||||
private Mock<IExecutionContext> _executionContext;
|
||||
private List<Tuple<DTWebApi.Issue, string>> _issues;
|
||||
private string _rootDirectory;
|
||||
private SetEnvFileCommand _setEnvFileCommand;
|
||||
private ITraceWriter _trace;
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_DirectoryNotFound()
|
||||
{
|
||||
base.TestDirectoryNotFound();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -27,7 +45,13 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_NotFound()
|
||||
{
|
||||
base.TestNotFound();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "file-not-found");
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -35,7 +59,15 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_EmptyFile()
|
||||
{
|
||||
base.TestEmptyFile();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "empty-file");
|
||||
var content = new List<string>();
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -43,7 +75,19 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Simple()
|
||||
{
|
||||
base.TestSimple();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV=MY VALUE",
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal("MY VALUE", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -51,7 +95,24 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Simple_SkipEmptyLines()
|
||||
{
|
||||
base.TestSimple_SkipEmptyLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_ENV=my value",
|
||||
string.Empty,
|
||||
"MY_ENV_2=my second value",
|
||||
string.Empty,
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal("my value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
Assert.Equal("my second value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -59,7 +120,19 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Simple_EmptyValue()
|
||||
{
|
||||
base.TestSimple_EmptyValue();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "simple-empty-value");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV=",
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -67,7 +140,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Simple_MultipleValues()
|
||||
{
|
||||
base.TestSimple_MultipleValues();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV=my value",
|
||||
"MY_ENV_2=",
|
||||
"MY_ENV_3=my third value",
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal("my value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
|
||||
Assert.Equal("my third value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -75,7 +164,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Simple_SpecialCharacters()
|
||||
{
|
||||
base.TestSimple_SpecialCharacters();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV==abc",
|
||||
"MY_ENV_2=def=ghi",
|
||||
"MY_ENV_3=jkl=",
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal("=abc", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
Assert.Equal("def=ghi", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
|
||||
Assert.Equal("jkl=", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -83,7 +188,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc()
|
||||
{
|
||||
base.TestHeredoc();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV<<EOF",
|
||||
"line one",
|
||||
"line two",
|
||||
"line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal($"line one{Environment.NewLine}line two{Environment.NewLine}line three", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -91,7 +212,20 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc_EmptyValue()
|
||||
{
|
||||
base.TestHeredoc_EmptyValue();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV<<EOF",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -99,52 +233,73 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc_SkipEmptyLines()
|
||||
{
|
||||
base.TestHeredoc_SkipEmptyLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_ENV<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
"MY_ENV_2<<EOF",
|
||||
"HELLO",
|
||||
"AGAIN",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal($"hello{Environment.NewLine}world", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
Assert.Equal($"HELLO{Environment.NewLine}AGAIN", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc_EdgeCases()
|
||||
public void SetEnvFileCommand_Heredoc_SpecialCharacters()
|
||||
{
|
||||
base.TestHeredoc_EdgeCases();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
// All of the following are not only valid, but quite plausible end markers.
|
||||
// Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
#pragma warning disable format
|
||||
[InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
|
||||
[InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
|
||||
[InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
|
||||
[InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
|
||||
#pragma warning restore format
|
||||
[InlineData("<<//++==")]
|
||||
[InlineData("contrivedBase64==")]
|
||||
[InlineData("khkIhPxsVA==")]
|
||||
[InlineData("D+Y8zE/EOw==")]
|
||||
[InlineData("wuOWG4S6FQ==")]
|
||||
[InlineData("7wigCJ//iw==")]
|
||||
[InlineData("uifTuYTs8K4=")]
|
||||
[InlineData("M7N2ITg/04c=")]
|
||||
[InlineData("Xhh+qp+Y6iM=")]
|
||||
[InlineData("5tdblQajc/b+EGBZXo0w")]
|
||||
[InlineData("jk/UMjIx/N0eVcQYOUfw")]
|
||||
[InlineData("/n5lsw73Cwl35Hfuscdz")]
|
||||
[InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
|
||||
public void SetEnvFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
|
||||
{
|
||||
base.TestHeredoc_EndMarkerVariations(validEndMarker);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc_EqualBeforeMultilineIndicator()
|
||||
{
|
||||
base.TestHeredoc_EqualBeforeMultilineIndicator();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV<<=EOF",
|
||||
"hello",
|
||||
"one",
|
||||
"=EOF",
|
||||
"MY_ENV_2<<<EOF",
|
||||
"hello",
|
||||
"two",
|
||||
"<EOF",
|
||||
"MY_ENV_3<<EOF",
|
||||
"hello",
|
||||
string.Empty,
|
||||
"three",
|
||||
string.Empty,
|
||||
"EOF",
|
||||
"MY_ENV_4<<EOF",
|
||||
"hello=four",
|
||||
"EOF",
|
||||
"MY_ENV_5<<EOF",
|
||||
" EOF",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(envFile, content);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(5, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal($"hello{Environment.NewLine}one", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
Assert.Equal($"hello{Environment.NewLine}two", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
|
||||
Assert.Equal($"hello{Environment.NewLine}{Environment.NewLine}three{Environment.NewLine}", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]);
|
||||
Assert.Equal($"hello=four", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_4"]);
|
||||
Assert.Equal($" EOF", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_5"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -152,15 +307,43 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc_MissingNewLine()
|
||||
{
|
||||
base.TestHeredoc_MissingNewLine();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV<<EOF",
|
||||
"line one",
|
||||
"line two",
|
||||
"line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(envFile, content, " ");
|
||||
var ex = Assert.Throws<Exception>(() => _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null));
|
||||
Assert.Contains("Matching delimiter not found", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLines()
|
||||
public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLinesEnv()
|
||||
{
|
||||
base.TestHeredoc_MissingNewLineMultipleLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var envFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV<<EOF",
|
||||
@"line one
|
||||
line two
|
||||
line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(envFile, content, " ");
|
||||
var ex = Assert.Throws<Exception>(() => _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null));
|
||||
Assert.Contains("EOF marker missing new line", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
@@ -169,9 +352,87 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetEnvFileCommand_Heredoc_PreservesNewline()
|
||||
{
|
||||
base.TestHeredoc_PreservesNewline();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var newline = "\n";
|
||||
var envFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_ENV<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(envFile, content, newline: newline);
|
||||
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
|
||||
Assert.Equal($"hello{newline}world", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void WriteContent(
|
||||
string path,
|
||||
List<string> content,
|
||||
string newline = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newline))
|
||||
{
|
||||
newline = Environment.NewLine;
|
||||
}
|
||||
|
||||
var encoding = new UTF8Encoding(true); // Emit BOM
|
||||
var contentStr = string.Join(newline, content);
|
||||
File.WriteAllText(path, contentStr, encoding);
|
||||
}
|
||||
|
||||
private TestHostContext Setup([CallerMemberName] string name = "")
|
||||
{
|
||||
_issues = new List<Tuple<DTWebApi.Issue, string>>();
|
||||
|
||||
var hostContext = new TestHostContext(this, name);
|
||||
|
||||
// Trace
|
||||
_trace = hostContext.GetTrace();
|
||||
|
||||
// Directory for test data
|
||||
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
|
||||
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
|
||||
Directory.CreateDirectory(workDirectory);
|
||||
_rootDirectory = Path.Combine(workDirectory, nameof(SetEnvFileCommandL0));
|
||||
Directory.CreateDirectory(_rootDirectory);
|
||||
|
||||
// Execution context
|
||||
_executionContext = new Mock<IExecutionContext>();
|
||||
_executionContext.Setup(x => x.Global)
|
||||
.Returns(new GlobalContext
|
||||
{
|
||||
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
|
||||
WriteDebug = true,
|
||||
});
|
||||
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
|
||||
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
|
||||
{
|
||||
var resolvedMessage = issue.Message;
|
||||
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
|
||||
{
|
||||
resolvedMessage = logOptions.LogMessageOverride;
|
||||
}
|
||||
_issues.Add(new(issue, resolvedMessage));
|
||||
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
|
||||
});
|
||||
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((string tag, string message) =>
|
||||
{
|
||||
_trace.Info($"{tag}{message}");
|
||||
});
|
||||
|
||||
// SetEnvFileCommand
|
||||
_setEnvFileCommand = new SetEnvFileCommand();
|
||||
_setEnvFileCommand.Initialize(hostContext);
|
||||
|
||||
return hostContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using DTWebApi = GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class SetOutputFileCommandL0 : FileCommandTestBase<SetOutputFileCommand>
|
||||
public sealed class SetOutputFileCommandL0
|
||||
{
|
||||
|
||||
protected override IDictionary<string, string> PostSetup()
|
||||
{
|
||||
var outputs = new Dictionary<string, string>();
|
||||
var reference = string.Empty;
|
||||
_executionContext.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference))
|
||||
.Callback((string name, string value, out string reference) =>
|
||||
{
|
||||
reference = value;
|
||||
outputs[name] = value;
|
||||
});
|
||||
|
||||
return outputs;
|
||||
|
||||
}
|
||||
private Mock<IExecutionContext> _executionContext;
|
||||
private List<Tuple<DTWebApi.Issue, string>> _issues;
|
||||
private Dictionary<string, string> _outputs;
|
||||
private string _rootDirectory;
|
||||
private SetOutputFileCommand _setOutputFileCommand;
|
||||
private ITraceWriter _trace;
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_DirectoryNotFound()
|
||||
{
|
||||
base.TestDirectoryNotFound();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _outputs.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -38,7 +46,13 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_NotFound()
|
||||
{
|
||||
base.TestNotFound();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _outputs.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -46,7 +60,15 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_EmptyFile()
|
||||
{
|
||||
base.TestEmptyFile();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "empty-file");
|
||||
var content = new List<string>();
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(0, _outputs.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -54,7 +76,19 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Simple()
|
||||
{
|
||||
base.TestSimple();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT=MY VALUE",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _outputs.Count);
|
||||
Assert.Equal("MY VALUE", _outputs["MY_OUTPUT"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -62,7 +96,24 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Simple_SkipEmptyLines()
|
||||
{
|
||||
base.TestSimple_SkipEmptyLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_OUTPUT=my value",
|
||||
string.Empty,
|
||||
"MY_OUTPUT_2=my second value",
|
||||
string.Empty,
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _outputs.Count);
|
||||
Assert.Equal("my value", _outputs["MY_OUTPUT"]);
|
||||
Assert.Equal("my second value", _outputs["MY_OUTPUT_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -70,7 +121,19 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Simple_EmptyValue()
|
||||
{
|
||||
base.TestSimple_EmptyValue();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple-empty-value");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT=",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _outputs.Count);
|
||||
Assert.Equal(string.Empty, _outputs["MY_OUTPUT"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -78,7 +141,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Simple_MultipleValues()
|
||||
{
|
||||
base.TestSimple_MultipleValues();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT=my value",
|
||||
"MY_OUTPUT_2=",
|
||||
"MY_OUTPUT_3=my third value",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _outputs.Count);
|
||||
Assert.Equal("my value", _outputs["MY_OUTPUT"]);
|
||||
Assert.Equal(string.Empty, _outputs["MY_OUTPUT_2"]);
|
||||
Assert.Equal("my third value", _outputs["MY_OUTPUT_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -86,7 +165,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Simple_SpecialCharacters()
|
||||
{
|
||||
base.TestSimple_SpecialCharacters();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "simple");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT==abc",
|
||||
"MY_OUTPUT_2=def=ghi",
|
||||
"MY_OUTPUT_3=jkl=",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(3, _outputs.Count);
|
||||
Assert.Equal("=abc", _outputs["MY_OUTPUT"]);
|
||||
Assert.Equal("def=ghi", _outputs["MY_OUTPUT_2"]);
|
||||
Assert.Equal("jkl=", _outputs["MY_OUTPUT_3"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -94,7 +189,23 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc()
|
||||
{
|
||||
base.TestHeredoc();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT<<EOF",
|
||||
"line one",
|
||||
"line two",
|
||||
"line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _outputs.Count);
|
||||
Assert.Equal($"line one{Environment.NewLine}line two{Environment.NewLine}line three", _outputs["MY_OUTPUT"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -102,7 +213,20 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_EmptyValue()
|
||||
{
|
||||
base.TestHeredoc_EmptyValue();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT<<EOF",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _outputs.Count);
|
||||
Assert.Equal(string.Empty, _outputs["MY_OUTPUT"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -110,52 +234,73 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_SkipEmptyLines()
|
||||
{
|
||||
base.TestHeredoc_SkipEmptyLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
string.Empty,
|
||||
"MY_OUTPUT<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
"MY_OUTPUT_2<<EOF",
|
||||
"HELLO",
|
||||
"AGAIN",
|
||||
"EOF",
|
||||
string.Empty,
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(2, _outputs.Count);
|
||||
Assert.Equal($"hello{Environment.NewLine}world", _outputs["MY_OUTPUT"]);
|
||||
Assert.Equal($"HELLO{Environment.NewLine}AGAIN", _outputs["MY_OUTPUT_2"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_EdgeCases()
|
||||
public void SetOutputFileCommand_Heredoc_SpecialCharacters()
|
||||
{
|
||||
base.TestHeredoc_EdgeCases();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
// All of the following are not only valid, but quite plausible end markers.
|
||||
// Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
#pragma warning disable format
|
||||
[InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
|
||||
[InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
|
||||
[InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
|
||||
[InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
|
||||
#pragma warning restore format
|
||||
[InlineData("<<//++==")]
|
||||
[InlineData("contrivedBase64==")]
|
||||
[InlineData("khkIhPxsVA==")]
|
||||
[InlineData("D+Y8zE/EOw==")]
|
||||
[InlineData("wuOWG4S6FQ==")]
|
||||
[InlineData("7wigCJ//iw==")]
|
||||
[InlineData("uifTuYTs8K4=")]
|
||||
[InlineData("M7N2ITg/04c=")]
|
||||
[InlineData("Xhh+qp+Y6iM=")]
|
||||
[InlineData("5tdblQajc/b+EGBZXo0w")]
|
||||
[InlineData("jk/UMjIx/N0eVcQYOUfw")]
|
||||
[InlineData("/n5lsw73Cwl35Hfuscdz")]
|
||||
[InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
|
||||
public void SetOutputFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
|
||||
{
|
||||
base.TestHeredoc_EndMarkerVariations(validEndMarker);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_EqualBeforeMultilineIndicator()
|
||||
{
|
||||
base.TestHeredoc_EqualBeforeMultilineIndicator();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT<<=EOF",
|
||||
"hello",
|
||||
"one",
|
||||
"=EOF",
|
||||
"MY_OUTPUT_2<<<EOF",
|
||||
"hello",
|
||||
"two",
|
||||
"<EOF",
|
||||
"MY_OUTPUT_3<<EOF",
|
||||
"hello",
|
||||
string.Empty,
|
||||
"three",
|
||||
string.Empty,
|
||||
"EOF",
|
||||
"MY_OUTPUT_4<<EOF",
|
||||
"hello=four",
|
||||
"EOF",
|
||||
"MY_OUTPUT_5<<EOF",
|
||||
" EOF",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(5, _outputs.Count);
|
||||
Assert.Equal($"hello{Environment.NewLine}one", _outputs["MY_OUTPUT"]);
|
||||
Assert.Equal($"hello{Environment.NewLine}two", _outputs["MY_OUTPUT_2"]);
|
||||
Assert.Equal($"hello{Environment.NewLine}{Environment.NewLine}three{Environment.NewLine}", _outputs["MY_OUTPUT_3"]);
|
||||
Assert.Equal($"hello=four", _outputs["MY_OUTPUT_4"]);
|
||||
Assert.Equal($" EOF", _outputs["MY_OUTPUT_5"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -163,7 +308,21 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_MissingNewLine()
|
||||
{
|
||||
base.TestHeredoc_MissingNewLine();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT<<EOF",
|
||||
"line one",
|
||||
"line two",
|
||||
"line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content, " ");
|
||||
var ex = Assert.Throws<Exception>(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
|
||||
Assert.Contains("Matching delimiter not found", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -171,7 +330,21 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_MissingNewLineMultipleLines()
|
||||
{
|
||||
base.TestHeredoc_MissingNewLineMultipleLines();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT<<EOF",
|
||||
@"line one
|
||||
line two
|
||||
line three",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content, " ");
|
||||
var ex = Assert.Throws<Exception>(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
|
||||
Assert.Contains("EOF marker missing new line", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
@@ -180,9 +353,96 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_PreservesNewline()
|
||||
{
|
||||
base.TestHeredoc_PreservesNewline();
|
||||
using (var hostContext = Setup())
|
||||
{
|
||||
var newline = "\n";
|
||||
var stateFile = Path.Combine(_rootDirectory, "heredoc");
|
||||
var content = new List<string>
|
||||
{
|
||||
"MY_OUTPUT<<EOF",
|
||||
"hello",
|
||||
"world",
|
||||
"EOF",
|
||||
};
|
||||
WriteContent(stateFile, content, newline: newline);
|
||||
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
|
||||
Assert.Equal(0, _issues.Count);
|
||||
Assert.Equal(1, _outputs.Count);
|
||||
Assert.Equal($"hello{newline}world", _outputs["MY_OUTPUT"]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void WriteContent(
|
||||
string path,
|
||||
List<string> content,
|
||||
string newline = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(newline))
|
||||
{
|
||||
newline = Environment.NewLine;
|
||||
}
|
||||
|
||||
var encoding = new UTF8Encoding(true); // Emit BOM
|
||||
var contentStr = string.Join(newline, content);
|
||||
File.WriteAllText(path, contentStr, encoding);
|
||||
}
|
||||
|
||||
private TestHostContext Setup([CallerMemberName] string name = "")
|
||||
{
|
||||
_issues = new List<Tuple<DTWebApi.Issue, string>>();
|
||||
_outputs = new Dictionary<string, string>();
|
||||
|
||||
var hostContext = new TestHostContext(this, name);
|
||||
|
||||
// Trace
|
||||
_trace = hostContext.GetTrace();
|
||||
|
||||
// Directory for test data
|
||||
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
|
||||
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
|
||||
Directory.CreateDirectory(workDirectory);
|
||||
_rootDirectory = Path.Combine(workDirectory, nameof(SetOutputFileCommandL0));
|
||||
Directory.CreateDirectory(_rootDirectory);
|
||||
|
||||
// Execution context
|
||||
_executionContext = new Mock<IExecutionContext>();
|
||||
_executionContext.Setup(x => x.Global)
|
||||
.Returns(new GlobalContext
|
||||
{
|
||||
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
|
||||
WriteDebug = true,
|
||||
});
|
||||
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
|
||||
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
|
||||
{
|
||||
var resolvedMessage = issue.Message;
|
||||
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
|
||||
{
|
||||
resolvedMessage = logOptions.LogMessageOverride;
|
||||
}
|
||||
_issues.Add(new(issue, resolvedMessage));
|
||||
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
|
||||
});
|
||||
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((string tag, string message) =>
|
||||
{
|
||||
_trace.Info($"{tag}{message}");
|
||||
});
|
||||
|
||||
var reference = string.Empty;
|
||||
_executionContext.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference))
|
||||
.Callback((string name, string value, out string reference) =>
|
||||
{
|
||||
reference = value;
|
||||
_outputs[name] = value;
|
||||
});
|
||||
|
||||
// SetOutputFileCommand
|
||||
_setOutputFileCommand = new SetOutputFileCommand();
|
||||
_setOutputFileCommand.Initialize(hostContext);
|
||||
|
||||
return hostContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.307.0
|
||||
2.304.0
|
||||
|
||||
Reference in New Issue
Block a user