Compare commits

...

21 Commits

Author SHA1 Message Date
Ferenc Hammerl
2a7e9ad3a7 Bump patch version for new release 2023-07-25 14:40:05 +02:00
Ferenc Hammerl
00e60cad27 Pass timeout in executioncontext instead of stepsrunner (#2714) (#2715)
* Pass timeout in executioncontext instead of stepsrunner

* Fix linting
2023-07-25 14:31:07 +02:00
JoannaaKL
f8ad46b347 Update releaseVersion 2023-07-24 13:06:36 +02:00
JoannaaKL
e30b9d6d12 Update runnerversion (#2711)
* Update runnerversion

* Update releaseNote.md
2023-07-24 13:00:47 +02:00
JoannaaKL
496904c0b7 Fix feature flag location (#2703) 2023-07-19 12:19:42 +02:00
Tingluo Huang
b91ad56f92 Check connectivity for endpoints requested by service. (#2691) 2023-07-17 08:36:20 -04:00
Ferenc Hammerl
f25c9dfba3 Fixes if:cancelled() composite steps not running and normal composite steps not interrupting when the job is cancelled. (#2638)
* Set composite step's action_status when job is cancelled

Also make composite step inherit timeout from parent

* Fix eof line
2023-07-10 13:32:30 +02:00
Tatyana Kostromskaya
7d432fb24c Prepare release 2.306.0 (#2682) 2023-07-07 13:41:46 +02:00
Tatyana Kostromskaya
e8ee6f7b1b Add warning to notify about forcing node16 (#2678)
* add warning

* fix tests

* aggregate to one warning

* fix linter

* fix comm
2023-07-07 11:35:54 +02:00
Cameron Booth
d4bbbb8419 Only add StepResult for Tasks, not the Job itself (#2620) 2023-06-30 04:17:46 -07:00
John Wesley Walker III
4ffd081aea Fixed a bug where a misplaced = character could bypass heredoc-style processing. (#2627)
* Fixed a bug where a misplaced `=` character could bypass heredoc-style processing.

Fixes https://github.com/github/c2c-actions/issues/6910

GitHub Docs for context:  https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings

* Consolidate near-identical FileCommand-related unit test classes. (#2672)
2023-06-29 12:52:05 +02:00
Nikola Jokic
c05e6748c3 Extend github context with host-workspace (#2517)
* hash files translation works with host context translated

* Change file encoding to utf8 for PipelineTemplateConstants.cs

* infer the feature flag on hash files based on the existence of the context

* encoded utf8 with bom

* rename host-workspace to host-work-directory and feature flag the new context var

* Added comment to explain ignore host work directory resolution

Co-authored-by: fhammerl <fhammerl@github.com>

* trigger pipeline

* Move to the github host-workspace context instead of the runner host-work-directory

* remove unused imports

---------

Co-authored-by: fhammerl <fhammerl@github.com>
2023-06-29 12:12:23 +02:00
Nikola Jokic
a2b7856c9c Fix error message reported on non-local action setup (#2668)
* Fix error message reported on non-local action setup

* Remove tostring left from string builder
2023-06-29 11:33:46 +02:00
Ferenc Hammerl
5f1c6f4708 Add 'http://' to http(s)_proxy if there is no protocol (#2663)
* Add 'http://' to http(s)_proxy if there is no protocol

Before this commit, these URIs used to be ignored (nullproxy).

The new behaviour aligns with go style http(s)_proxy

* Update src/Runner.Sdk/RunnerWebProxy.cs

Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>

* Assert that original protocol is preserved

* Use startsWith for testing protocol

* Only modify proxy if useful

---------

Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
2023-06-20 15:53:44 +02:00
Nikola Jokic
8415f13bab removed throw and else on container action handler (#1873)
* removed throw and else on container action handler

* repaired merge resolution error
2023-06-16 21:53:52 -04:00
Yashwanth Anantharaju
471e3ae2d9 Reduce token service and unnecessary calls - send token to redirects (#2660)
* handle redirects with token

* scope to broker

* lint
2023-06-16 13:23:13 +00:00
Dylan Geraci
1096b975e4 Send environment url to Run Service (#2650)
* add EnvironmentUrl to CompleteJobRequest

* Send environment url to Run Service

* Fix whitespace

* Fix test

* Fix more whitespace

* Apply suggestions from code review

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

* Apply suggestion from code review

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

---------

Co-authored-by: Tingluo Huang <tingluohuang@github.com>
2023-06-16 04:13:08 +00:00
JoannaaKL
282ba4cfc8 Just dont log these errors (#2656)
* Dont log template errors
2023-06-13 21:10:53 -04:00
Yashwanth Anantharaju
b737a5ac5c remove job completion (#2659) 2023-06-13 20:50:36 +00:00
Tingluo Huang
20721bc950 Prepare runner release 2.305.0. (#2657)
* Prepare runner release 2.305.0.

* Update releaseNote.md

Co-authored-by: John Sudol <24583161+johnsudol@users.noreply.github.com>

* Update releaseNote.md

Co-authored-by: John Sudol <24583161+johnsudol@users.noreply.github.com>

---------

Co-authored-by: John Sudol <24583161+johnsudol@users.noreply.github.com>
2023-06-13 12:57:37 -04:00
Bassem Dghaidi
fde86b0666 Switch from Debian to Ubuntu 22.04 (#2651) 2023-06-13 10:12:22 -04:00
36 changed files with 1141 additions and 1120 deletions

View File

@@ -1,4 +1,5 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0 as build
# Source: https://github.com/dotnet/dotnet-docker
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-jammy as build
ARG RUNNER_VERSION
ARG RUNNER_ARCH="x64"
@@ -22,7 +23,7 @@ RUN export DOCKER_ARCH=x86_64 \
&& tar zxvf docker.tgz \
&& rm -rf docker.tgz
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-jammy
ENV DEBIAN_FRONTEND=noninteractive
ENV RUNNER_MANUALLY_TRAP_SIG=1
@@ -31,6 +32,7 @@ ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends \
sudo \
lsb-release \
&& rm -rf /var/lib/apt/lists/*
RUN adduser --disabled-password --gecos "" --uid 1001 runner \

View File

@@ -1,18 +1,9 @@
## 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
- 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)
- 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)
## Misc
- Bump container hooks version to 0.3.1 in runner image (#2496)
- Runner changes to communicate with vNext services (#2487, #2500, #2505, #2541, #2547)
- Collect telemetry on GitHub-related HTTP requests (#2691)
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.

View File

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

View File

@@ -155,6 +155,7 @@ 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";
}
@@ -171,6 +172,8 @@ namespace GitHub.Runner.Common
public static readonly string UnsupportedSummarySize = "$GITHUB_STEP_SUMMARY upload aborted, supports content up to a size of {0}k, got {1}k. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
public static readonly string SummaryUploadError = "$GITHUB_STEP_SUMMARY upload aborted, an error occurred when uploading the summary. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
public static readonly string Node12DetectedAfterEndOfLife = "Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: {0}. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.";
public static readonly string EnforcedNode12DetectedAfterEndOfLife = "The following actions uses node12 which is deprecated and will be forced to run on node16: {0}. For more info: https://github.blog/changelog/2023-06-13-github-actions-all-actions-will-run-on-node16-instead-of-node12-by-default/";
public static readonly string EnforcedNode12DetectedAfterEndOfLifeEnvVariable = "Node16ForceActionsWarnings";
}
public static class RunnerEvent

View File

@@ -26,6 +26,7 @@ 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);
@@ -70,11 +71,12 @@ 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, cancellationToken), cancellationToken);
async () => await _runServiceHttpClient.CompleteJobAsync(requestUri, planId, jobId, result, outputs, stepResults, jobAnnotations, environmentUrl, cancellationToken), cancellationToken);
}
public Task<RenewJobResponse> RenewJobAsync(Guid planId, Guid jobId, CancellationToken cancellationToken)

View File

@@ -1077,23 +1077,7 @@ namespace GitHub.Runner.Listener
if (this._isRunServiceJob)
{
var runServer = await GetRunServerAsync(systemConnection);
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = detailInfo };
var unhandledAnnotation = unhandledExceptionIssue.ToAnnotation();
var jobAnnotations = new List<Annotation>();
if (unhandledAnnotation.HasValue)
{
jobAnnotations.Add(unhandledAnnotation.Value);
}
try
{
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, CancellationToken.None);
}
catch (Exception ex)
{
Trace.Error("Fail to raise job completion back to service.");
Trace.Error(ex);
}
Trace.Verbose($"Skip CompleteJobRequestAsync call from Listener because it's RunService job");
return;
}
@@ -1193,7 +1177,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, CancellationToken.None);
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, TaskResult.Failed, outputs: null, stepResults: null, jobAnnotations: jobAnnotations, environmentUrl: null, CancellationToken.None);
}
catch (Exception ex)
{

View File

@@ -69,6 +69,10 @@ 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;
@@ -99,6 +103,10 @@ 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;
@@ -240,5 +248,20 @@ 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;
}
}
}

View File

@@ -64,7 +64,7 @@ namespace GitHub.Runner.Sdk
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("USE_BROKER_FLOW")))
{
settings.AllowAutoRedirect = true;
settings.AllowAutoRedirectForBroker = true;
}
// Remove Invariant from the list of accepted languages.

View File

@@ -1042,7 +1042,7 @@ namespace GitHub.Runner.Worker
if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Container)
{
var containerAction = actionDefinitionData.Execution as ContainerActionExecutionData;
if (containerAction.Image.EndsWith("Dockerfile") || containerAction.Image.EndsWith("dockerfile"))
if (DockerUtil.IsDockerfile(containerAction.Image))
{
var dockerFileFullPath = Path.Combine(actionEntryDirectory, containerAction.Image);
executionContext.Debug($"Dockerfile for action: '{dockerFileFullPath}'.");
@@ -1127,8 +1127,16 @@ namespace GitHub.Runner.Worker
}
else
{
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?");
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}'.");
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace GitHub.Runner.Worker.Container
@@ -65,6 +66,16 @@ 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))

View File

@@ -78,11 +78,12 @@ 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);
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 CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null);
// logging
@@ -264,6 +265,14 @@ namespace GitHub.Runner.Worker
}
}
public ExecutionContext Parent
{
get
{
return _parentExecutionContext;
}
}
public JobContext JobContext
{
get
@@ -348,7 +357,8 @@ namespace GitHub.Runner.Worker
bool isEmbedded = false,
CancellationTokenSource cancellationTokenSource = null,
Guid embeddedId = default(Guid),
string siblingScopeName = null)
string siblingScopeName = null,
TimeSpan? timeout = null)
{
Trace.Entering();
@@ -377,6 +387,12 @@ 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)
@@ -406,7 +422,7 @@ namespace GitHub.Runner.Worker
/// <summary>
/// An embedded execution context shares the same record ID, record name, logger,
/// and a linked cancellation token.
/// but NOT the cancellation token (just like workflow steps contexts - they don't share a token)
/// </summary>
public IExecutionContext CreateEmbeddedChild(
string scopeName,
@@ -416,7 +432,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: CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token), intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName);
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());
}
public void Start(string currentOperation = null)
@@ -477,28 +493,32 @@ namespace GitHub.Runner.Worker
PublishStepTelemetry();
var stepResult = new StepResult
if (_record.RecordType == "Task")
{
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)
var stepResult = new StepResult
{
stepResult.Annotations.Add(annotation.Value);
}
});
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);
}
Global.StepsResult.Add(stepResult);
if (Root != this)
{
@@ -593,9 +613,33 @@ 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)
@@ -1355,6 +1399,12 @@ 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();
@@ -1401,7 +1451,15 @@ namespace GitHub.Runner.Worker
public void Error(string format, params Object[] args)
{
_executionContext.Error(string.Format(CultureInfo.CurrentCulture, format, 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));
}
}
public void Info(string format, params Object[] args)
@@ -1414,8 +1472,18 @@ 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]";

View File

@@ -26,11 +26,18 @@ namespace GitHub.Runner.Worker.Expressions
ArgUtil.NotNull(githubContextData, nameof(githubContextData));
var githubContext = githubContextData as DictionaryContextData;
ArgUtil.NotNull(githubContext, nameof(githubContext));
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out var workspace);
if (!githubContext.TryGetValue(PipelineTemplateConstants.HostWorkspace, out var workspace))
{
githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out workspace);
}
ArgUtil.NotNull(workspace, nameof(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;

View File

@@ -322,21 +322,9 @@ namespace GitHub.Runner.Worker
var equalsIndex = line.IndexOf("=", StringComparison.Ordinal);
var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal);
// 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))
// 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))
{
var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None);
if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1]))
@@ -364,6 +352,18 @@ 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}'");

View File

@@ -310,6 +310,7 @@ 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

View File

@@ -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 (Data.Image.EndsWith("Dockerfile") || Data.Image.EndsWith("dockerfile"))
else if (DockerUtil.IsDockerfile(Data.Image))
{
// ensure docker file exist
dockerFile = Path.Combine(ActionDirectory, Data.Image);

View File

@@ -68,6 +68,28 @@ namespace GitHub.Runner.Worker.Handlers
bool isOptOut = isWorkflowOptOutSet ? StringUtil.ConvertToBoolean(workflowOptOut) : isLocalOptOut;
if (!isOptOut)
{
var repoAction = action as Pipelines.RepositoryPathReference;
if (repoAction != null)
{
var warningActions = new HashSet<string>();
if (executionContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode12DetectedAfterEndOfLifeEnvVariable, out var node16ForceWarnings))
{
warningActions = StringUtil.ConvertFromJson<HashSet<string>>(node16ForceWarnings);
}
var repoActionFullName = "";
if (string.IsNullOrEmpty(repoAction.Name))
{
repoActionFullName = repoAction.Path; // local actions don't have a 'Name'
}
else
{
repoActionFullName = $"{repoAction.Name}/{repoAction.Path ?? string.Empty}".TrimEnd('/') + $"@{repoAction.Ref}";
}
warningActions.Add(repoActionFullName);
executionContext.Global.Variables.Set("Node16ForceActionsWarnings", StringUtil.ConvertToJson(warningActions));
}
nodeData.NodeVersion = "node16";
}
}

View File

@@ -4,6 +4,7 @@ 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;
@@ -15,6 +16,7 @@ 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
@@ -34,12 +36,13 @@ namespace GitHub.Runner.Worker
public interface IJobExtension : IRunnerService
{
Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message);
void FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc);
Task 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();
@@ -175,7 +178,14 @@ namespace GitHub.Runner.Worker
context.Debug("Update context data");
string _workDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
context.SetRunnerContext("workspace", Path.Combine(_workDirectory, trackingConfig.PipelineDirectory));
context.SetGitHubContext("workspace", Path.Combine(_workDirectory, trackingConfig.WorkspaceDirectory));
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);
// Temporary hack for GHES alpha
var configurationStore = HostContext.GetService<IConfigurationStore>();
@@ -421,6 +431,22 @@ 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)
@@ -465,7 +491,7 @@ namespace GitHub.Runner.Worker
return reference;
}
public void FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc)
public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc)
{
Trace.Entering();
ArgUtil.NotNull(jobContext, nameof(jobContext));
@@ -642,6 +668,28 @@ 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)
{
@@ -657,6 +705,37 @@ 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)

View File

@@ -7,6 +7,7 @@ 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;
@@ -228,7 +229,7 @@ namespace GitHub.Runner.Worker
finally
{
Trace.Info("Finalize job.");
jobExtension.FinalizeJob(jobContext, message, jobStartTimeUtc);
await jobExtension.FinalizeJob(jobContext, message, jobStartTimeUtc);
}
Trace.Info($"Job result after all job steps finish: {jobContext.Result ?? TaskResult.Succeeded}");
@@ -283,6 +284,13 @@ 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>();
@@ -290,7 +298,7 @@ namespace GitHub.Runner.Worker
{
try
{
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, default);
await runServer.CompleteJobAsync(message.Plan.PlanId, message.JobId, result, jobContext.JobOutputs, jobContext.Global.StepsResult, jobContext.Global.JobAnnotations, environmentUrl, default);
return result;
}
catch (Exception ex)
@@ -366,6 +374,12 @@ namespace GitHub.Runner.Worker
jobContext.Warning(string.Format(Constants.Runner.Node12DetectedAfterEndOfLife, actions));
}
if (jobContext.Global.Variables.TryGetValue(Constants.Runner.EnforcedNode12DetectedAfterEndOfLifeEnvVariable, out var node16ForceWarnings))
{
var actions = string.Join(", ", StringUtil.ConvertFromJson<HashSet<string>>(node16ForceWarnings));
jobContext.Warning(string.Format(Constants.Runner.EnforcedNode12DetectedAfterEndOfLife, actions));
}
try
{
await ShutdownQueue(throwOnFailure: true);

View File

@@ -213,7 +213,26 @@ namespace GitHub.Services.Common
// ConfigureAwait(false) enables the continuation to be run outside any captured
// SyncronizationContext (such as ASP.NET's) which keeps things from deadlocking...
response = await m_messageInvoker.SendAsync(request, tokenSource.Token).ConfigureAwait(false);
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;
}
traceInfo?.TraceRequestSendTime();

View File

@@ -110,6 +110,16 @@ namespace GitHub.Services.Common
set;
}
/// <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.
/// The default value is true.

View File

@@ -82,5 +82,6 @@ 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";
}
}

View File

@@ -9,5 +9,8 @@ namespace GitHub.DistributedTask.WebApi
[EnumMember]
ActionCommand = 1,
[EnumMember]
ConnectivityCheck = 2,
}
}

View File

@@ -26,5 +26,8 @@ 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; }
}
}

View File

@@ -100,6 +100,7 @@ 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");
@@ -110,7 +111,8 @@ namespace GitHub.Actions.RunService.WebApi
Conclusion = result,
Outputs = outputs,
StepResults = stepResults,
Annotations = jobAnnotations
Annotations = jobAnnotations,
EnvironmentUrl = environmentUrl,
};
requestUri = new Uri(requestUri, "completejob");

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using GitHub.Runner.Worker.Container;
using Xunit;
@@ -145,6 +145,49 @@ 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")]

View File

@@ -186,8 +186,8 @@ namespace GitHub.Runner.Common.Tests
{
try
{
Environment.SetEnvironmentVariable("http_proxy", "127.0.0.1:7777");
Environment.SetEnvironmentVariable("https_proxy", "127.0.0.1");
Environment.SetEnvironmentVariable("http_proxy", "#fragment");
Environment.SetEnvironmentVariable("https_proxy", "#fragment");
var proxy = new RunnerWebProxy();
Assert.Null(proxy.HttpProxyAddress);
@@ -206,6 +206,68 @@ 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")]

View File

@@ -1,10 +1,21 @@
using System.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using Xunit;
using GitHub.Runner.Sdk;
using System.Runtime.CompilerServices;
using System.Linq;
namespace GitHub.Runner.Common.Tests
{
public enum LineEndingType
{
Native,
Linux = 0x__0A,
Windows = 0x0D0A
}
public static class TestUtil
{
private const string Src = "src";
@@ -41,5 +52,24 @@ 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);
}
}
}

View File

@@ -124,7 +124,7 @@ namespace GitHub.Runner.Common.Tests.Worker
"",
"## This is more markdown content",
};
WriteContent(stepSummaryFile, content);
TestUtil.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",
};
WriteContent(stepSummaryFile, content);
TestUtil.WriteContent(stepSummaryFile, content);
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
@@ -167,21 +167,6 @@ 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);

View File

@@ -821,8 +821,7 @@ namespace GitHub.Runner.Common.Tests.Worker
ec.Complete();
// Assert.
Assert.Equal(1, ec.Global.StepsResult.Count);
Assert.Equal(TaskResult.Succeeded, ec.Global.StepsResult.Single().Conclusion);
Assert.Equal(0, ec.Global.StepsResult.Count);
}
}

View File

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

View File

@@ -1,13 +1,13 @@
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using Moq;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xunit;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using Moq;
using Xunit;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Common.Tests.Worker
@@ -105,6 +105,18 @@ 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);
@@ -231,7 +243,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void UploadDiganosticLogIfEnvironmentVariableSet()
public async Task UploadDiganosticLogIfEnvironmentVariableSet()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -244,7 +256,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
_diagnosticLogManager.Verify(x =>
x.UploadDiagnosticLogs(
@@ -259,7 +271,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void DontUploadDiagnosticLogIfEnvironmentVariableFalse()
public async Task DontUploadDiagnosticLogIfEnvironmentVariableFalse()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -272,7 +284,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
_diagnosticLogManager.Verify(x =>
x.UploadDiagnosticLogs(
@@ -287,14 +299,14 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void DontUploadDiagnosticLogIfEnvironmentVariableMissing()
public async Task DontUploadDiagnosticLogIfEnvironmentVariableMissing()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
_diagnosticLogManager.Verify(x =>
x.UploadDiagnosticLogs(
@@ -309,7 +321,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureFinalizeJobRunsIfMessageHasNoEnvironmentUrl()
public async Task EnsureFinalizeJobRunsIfMessageHasNoEnvironmentUrl()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -322,7 +334,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
}
@@ -331,7 +343,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureFinalizeJobHandlesNullEnvironmentUrl()
public async Task EnsureFinalizeJobHandlesNullEnvironmentUrl()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -347,7 +359,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
}
@@ -356,7 +368,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureFinalizeJobHandlesNullEnvironment()
public async Task EnsureFinalizeJobHandlesNullEnvironment()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -369,7 +381,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
}
@@ -397,7 +409,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var hookStart = result.First() as JobExtensionRunner;
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(Constants.Hooks.JobStartedStepName, hookStart.DisplayName);
Assert.Equal(Constants.Hooks.JobCompletedStepName, (_jobEc.PostJobSteps.Last() as JobExtensionRunner).DisplayName);
@@ -410,7 +422,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureNoPreAndPostHookSteps()
public async Task EnsureNoPreAndPostHookSteps()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -425,7 +437,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var x = _jobEc.JobSteps;
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
Assert.Equal(0, _jobEc.PostJobSteps.Count);

View File

@@ -99,7 +99,8 @@ 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, null,
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"),
messageType: messageType);
message.Variables[Constants.Variables.System.Culture] = "en-US";
message.Resources.Endpoints.Add(new ServiceEndpoint()

View File

@@ -1,44 +1,27 @@
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
public sealed class SaveStateFileCommandL0 : FileCommandTestBase<SaveStateFileCommand>
{
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;
protected override IDictionary<string, string> PostSetup()
{
var intraActionState = new Dictionary<string, string>();
_executionContext.Setup(x => x.IntraActionState).Returns(intraActionState);
return intraActionState;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SaveStateFileCommand_DirectoryNotFound()
{
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);
}
base.TestDirectoryNotFound();
}
[Fact]
@@ -46,13 +29,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_NotFound()
{
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);
}
base.TestNotFound();
}
[Fact]
@@ -60,15 +37,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_EmptyFile()
{
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);
}
base.TestEmptyFile();
}
[Fact]
@@ -76,19 +45,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple()
{
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"]);
}
base.TestSimple();
}
[Fact]
@@ -96,24 +53,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_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"]);
}
base.TestSimple_SkipEmptyLines();
}
[Fact]
@@ -121,19 +61,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_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"]);
}
base.TestSimple_EmptyValue();
}
[Fact]
@@ -141,23 +69,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_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"]);
}
base.TestSimple_MultipleValues();
}
[Fact]
@@ -165,23 +77,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_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"]);
}
base.TestSimple_SpecialCharacters();
}
[Fact]
@@ -189,23 +85,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc()
{
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"]);
}
base.TestHeredoc();
}
[Fact]
@@ -213,20 +93,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_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"]);
}
base.TestHeredoc_EmptyValue();
}
[Fact]
@@ -234,73 +101,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_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"]);
}
base.TestHeredoc_SkipEmptyLines();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_SpecialCharacters()
public void SaveStateFileCommand_Heredoc_EdgeCases()
{
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"]);
}
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();
}
[Fact]
@@ -308,21 +154,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_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);
}
base.TestHeredoc_MissingNewLine();
}
[Fact]
@@ -330,21 +162,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_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);
}
base.TestHeredoc_MissingNewLineMultipleLines();
}
#if OS_WINDOWS
@@ -353,90 +171,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_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"]);
}
base.TestHeredoc_PreservesNewline();
}
#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;
}
}
}

View File

@@ -1,43 +1,25 @@
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
public sealed class SetEnvFileCommandL0 : FileCommandTestBase<SetEnvFileCommand>
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private string _rootDirectory;
private SetEnvFileCommand _setEnvFileCommand;
private ITraceWriter _trace;
protected override IDictionary<string, string> PostSetup()
{
return _executionContext.Object.Global.EnvironmentVariables;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetEnvFileCommand_DirectoryNotFound()
{
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);
}
base.TestDirectoryNotFound();
}
[Fact]
@@ -45,13 +27,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_NotFound()
{
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);
}
base.TestNotFound();
}
[Fact]
@@ -59,15 +35,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_EmptyFile()
{
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);
}
base.TestEmptyFile();
}
[Fact]
@@ -75,19 +43,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple()
{
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"]);
}
base.TestSimple();
}
[Fact]
@@ -95,24 +51,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_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"]);
}
base.TestSimple_SkipEmptyLines();
}
[Fact]
@@ -120,19 +59,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_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"]);
}
base.TestSimple_EmptyValue();
}
[Fact]
@@ -140,23 +67,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_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"]);
}
base.TestSimple_MultipleValues();
}
[Fact]
@@ -164,23 +75,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_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"]);
}
base.TestSimple_SpecialCharacters();
}
[Fact]
@@ -188,23 +83,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc()
{
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"]);
}
base.TestHeredoc();
}
[Fact]
@@ -212,20 +91,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_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"]);
}
base.TestHeredoc_EmptyValue();
}
[Fact]
@@ -233,73 +99,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_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"]);
}
base.TestHeredoc_SkipEmptyLines();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_SpecialCharacters()
public void SetEnvFileCommand_Heredoc_EdgeCases()
{
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"]);
}
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();
}
[Fact]
@@ -307,43 +152,15 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_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);
}
base.TestHeredoc_MissingNewLine();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLinesEnv()
public void SetEnvFileCommand_Heredoc_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);
}
base.TestHeredoc_MissingNewLineMultipleLines();
}
#if OS_WINDOWS
@@ -352,87 +169,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_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"]);
}
base.TestHeredoc_PreservesNewline();
}
#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;
}
}
}

View File

@@ -1,44 +1,36 @@
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
public sealed class SetOutputFileCommandL0 : FileCommandTestBase<SetOutputFileCommand>
{
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;
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;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_DirectoryNotFound()
{
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);
}
base.TestDirectoryNotFound();
}
[Fact]
@@ -46,13 +38,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_NotFound()
{
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);
}
base.TestNotFound();
}
[Fact]
@@ -60,15 +46,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_EmptyFile()
{
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);
}
base.TestEmptyFile();
}
[Fact]
@@ -76,19 +54,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple()
{
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"]);
}
base.TestSimple();
}
[Fact]
@@ -96,24 +62,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_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"]);
}
base.TestSimple_SkipEmptyLines();
}
[Fact]
@@ -121,19 +70,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_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"]);
}
base.TestSimple_EmptyValue();
}
[Fact]
@@ -141,23 +78,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_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"]);
}
base.TestSimple_MultipleValues();
}
[Fact]
@@ -165,23 +86,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_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"]);
}
base.TestSimple_SpecialCharacters();
}
[Fact]
@@ -189,23 +94,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc()
{
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"]);
}
base.TestHeredoc();
}
[Fact]
@@ -213,20 +102,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_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"]);
}
base.TestHeredoc_EmptyValue();
}
[Fact]
@@ -234,73 +110,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_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"]);
}
base.TestHeredoc_SkipEmptyLines();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_SpecialCharacters()
public void SetOutputFileCommand_Heredoc_EdgeCases()
{
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"]);
}
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();
}
[Fact]
@@ -308,21 +163,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_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);
}
base.TestHeredoc_MissingNewLine();
}
[Fact]
@@ -330,21 +171,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_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);
}
base.TestHeredoc_MissingNewLineMultipleLines();
}
#if OS_WINDOWS
@@ -353,96 +180,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_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"]);
}
base.TestHeredoc_PreservesNewline();
}
#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;
}
}
}

View File

@@ -1 +1 @@
2.304.0
2.307.1