Compare commits

..

18 Commits

Author SHA1 Message Date
Lokesh Gopu
38b096aa4e Update runnerversion 2020-02-28 13:24:01 -05:00
Lokesh Gopu
110c8775a3 Update runnerversion 2020-02-28 12:33:33 -05:00
Josh Gross
2efd6f70e2 Use the Uri Scheme in the register runner url (#345) 2020-02-25 18:30:33 -05:00
Lokesh Gopu
a6f144b014 Update Runner Register GitHub API URL to Support Org-level Runner (#339)
* Update GitHub API URL

* pr comments

* Updated GitHub API URL
2020-02-24 09:15:15 -05:00
eric sciple
5294a3ee06 commands translate file path from container action (#331) 2020-02-12 21:07:43 -05:00
Tingluo Huang
745b90a8b2 Revert "Update Base64 Encoders to deal with suffixes (#284)" (#330)
This reverts commit c45aebc9ab.
2020-02-12 14:26:30 -05:00
Tingluo Huang
0db908da8d Use authenticate endpoint for testing runner connection. (#311)
* use authenticate endpoint for testing runner connection.

* PR feedback.
2020-02-05 16:56:38 -05:00
Thomas Boop
68de3a94be Remove Temporary Build Step (#316)
* Remove Temporary Build Step

* Updated dev.sh to set path for find
2020-02-04 12:59:49 -05:00
Tingluo Huang
a0a590fb48 setup/evaluate env context after setup steps context. (#309) 2020-01-30 22:14:14 -05:00
Christopher Johnson
87a232c477 Fix windows directions in release notes (#307) 2020-01-29 12:58:09 -05:00
TingluoHuang
a3c2479a29 prepare 2.165.0 runner release. 2020-01-27 22:14:06 -05:00
Thomas Boop
c45aebc9ab Update Base64 Encoders to deal with suffixes (#284)
* Update Base64 Encoders to deal with suffixes

* Set UriDataEscape to public for unit tests
2020-01-27 21:38:31 -05:00
Thomas Boop
b676ab3d33 Add ADR For Base64 Masking Improvements (#297)
* Base64 Secret Masking ADR

* slight addendums

* Update and rename 0000-base64-masking-trailing-characters.md to 0297-base64-masking-trailing-characters.md
2020-01-27 21:38:01 -05:00
Tingluo Huang
0a6bac355d include step.env as part of env context. (#300) 2020-01-27 15:54:28 -05:00
Tingluo Huang
eb78d19b17 Set both http_proxy and HTTP_PROXY env for runner/worker processes. (#298) 2020-01-27 11:04:35 -05:00
David Kale
17970ad1f9 Set http_proxy and related env vars for containers (#304)
* WIP add in basic passthrough for proxy env vars

* Add http_proxy vars after container env is created
2020-01-27 10:56:18 -05:00
Alberto Gimeno
2e0e8eb822 Change prompt message when removing a runner (#303)
* Change prompt message to be consistent with the UI/API

* Update test
2020-01-26 18:40:06 -05:00
Tingluo Huang
2a506cc556 Update 0278-env-context.md (#299) 2020-01-22 20:09:09 -05:00
35 changed files with 593 additions and 640 deletions

View File

@@ -43,14 +43,6 @@ jobs:
steps:
- uses: actions/checkout@v1
# Set Path workaround for https://github.com/actions/virtual-environments/issues/263
- run: |
echo "::add-path::C:\Program Files\Git\mingw64\bin"
echo "::add-path::C:\Program Files\Git\usr\bin"
echo "::add-path::C:\Program Files\Git\bin"
if: matrix.os == 'windows-latest'
name: "Temp step to Set Path for Windows"
# Build runner layout
- name: Build & Layout Release
run: |

View File

@@ -15,6 +15,7 @@ User wants to reference workflow variables defined in workflow yaml file for act
Runner will create and populate the `env` context for every job execution using following logic:
1. On job start, create `env` context with any environment variables in the job message, these are env defined in customer's YAML file's job/workflow level `env` section.
2. Update `env` context when customer use `::set-env::` to set env at the runner level.
3. Update `env` context with step's `env` block before each step runs.
The `env` context is only available in the runner, customer can't use the `env` context in any server evaluation part, just like the `runner` context
@@ -22,17 +23,25 @@ Example yaml:
```yaml
env:
env1: 100
env1: 10
env2: 20
env3: 30
jobs:
build:
env:
env1: 100
env2: 200
runs-on: ubuntu-latest
steps:
- run: |
echo ${{ env.env1 }}
if: env.env2 == 200
name: ${{ env.env1 }}_${{ env.env2 }}
echo ${{ env.env1 }} // 1000
echo $env1 // 1000
echo $env2 // 200
echo $env3 // 30
if: env.env2 == 200 // true
name: ${{ env.env1 }}_${{ env.env2 }} //1000_200
env:
env1: 1000
```
### Don't populate the `env` context with environment variables from runner machine.
@@ -48,4 +57,4 @@ build:
- uses: docker://ubuntu:18.04
with:
args: echo ${{env.USER}} <- what should customer expect this output? runner/root
```
```

View File

@@ -0,0 +1,48 @@
# ADR 0297: Base64 Masking Trailing Characters
**Date** 2020-01-21
**Status** Proposed
## Context
The Runner registers a number of Value Encoders, which mask various encodings of a provided secret. Currently, we register a 3 base64 Encoders:
- The base64 encoded secret
- The secret with the first character removed then base64 encoded
- The secret with the first two characters removed then base64 encoded
This gives us good coverage across the board for secrets and secrets with a prefix (i.e. `base64($user:$pass)`).
However, we don't have great coverage for cases where the secret has a string appended to it before it is base64 encoded (i.e.: `base64($pass\n))`).
Most notably we've seen this as a result of user error where a user accidentially appends a newline or space character before encoding their secret in base64.
## Decision
### Trim end characters
We are going to modify all existing base64 encoders to trim information before registering as a secret.
We will trim:
- `=` from the end of all base64 strings. This is a padding character that contains no information.
- Based on the number of `=`'s at the end of a base64 string, a malicious user could predict the length of the original secret modulo 3.
- If a user saw `***==`, they would know the secret could be 1,4,7,10... characters.
- If a string contains `=` we will also trim the last non-padding character from the base64 secret.
- This character can change if a string is appended to the secret before the encoding.
### Register a fourth encoder
We will also add back in the original base64 encoded secret encoder for four total encoders:
- The base64 encoded secret
- The base64 encoded secret trimmed
- The secret with the first character removed then base64 encoded and trimmed
- The secret with the first two characters removed then base64 encoded and trimmed
This allows us to fully cover the most common scenario where a user base64 encodes their secret and expects the entire thing to be masked.
This will result in us only revealing length or bit information when a prefix or suffix is added to a secret before encoding.
## Consequences
- In the case where a secret has a prefix or suffix added before base64 encoding, we may now reveal up to 20 bits of information and the length of the original string modulo 3, rather then the original 16 bits and no length information
- Secrets with a suffix appended before encoding will now be masked across the board. Previously it was only masked if it was a multiple of 3 characters
- Performance will suffer in a neglible way

View File

@@ -1,17 +1,30 @@
## Features
- Remove runner flow: Change from PAT to "deletion token" in prompt (#225)
- Expose github.run_id and github.run_number to action runtime env. (#224)
- Expose whether debug is on/off via RUNNER_DEBUG. (#253)
- Upload log on runner when worker get killed due to cancellation timeout. (#255)
- Update config.sh/cmd --help documentation (#282)
- Set http_proxy and related env vars for job/service containers (#304)
- Set both http_proxy and HTTP_PROXY env for runner/worker processes. (#298)
## Bugs
- Clean up error messages for container scenarios (#221)
- Pick shell from prependpath (#231)
- Verify runner Windows service hash started successfully after configuration (#236)
- Detect source file path in L0 without using env. (#257)
- Handle escaped '%' in commands data section (#200)
- Allow container to be null/empty during matrix expansion (#266)
- Translate problem matcher file to host path (#272)
- Change hashFiles() expression function to use @actions/glob. (#268)
- Default post-job action's condition to always(). (#293)
- Support action.yaml file as action's entry file (#288)
- Trace javascript action exit code to debug instead of user logs (#290)
- Change prompt message when removing a runner to lines up with GitHub.com UI (#303)
- Include step.env as part of env context. (#300)
- Update Base64 Encoders to deal with suffixes (#284)
## Misc
- Runner code cleanup (#218 #227, #228, #229, #230)
- Consume dotnet core 3.1 in runner. (#213)
- Move .sln file under ./src (#238)
- Treat warnings as errors during compile (#249)
## Windows x64
We recommend configuring the runner under "<DRIVE>:\actions-runner". This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows
We recommend configuring the runner in a root folder of the Windows drive (e.g. "C:\actions-runner"). This will help avoid issues related to service identity folder permissions and long file path restrictions on Windows
```
// Create a folder under the drive root
mkdir \actions-runner ; cd \actions-runner
@@ -19,7 +32,7 @@ mkdir \actions-runner ; cd \actions-runner
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v<RUNNER_VERSION>/actions-runner-win-x64-<RUNNER_VERSION>.zip -OutFile actions-runner-win-x64-<RUNNER_VERSION>.zip
// Extract the installer
Add-Type -AssemblyName System.IO.Compression.FileSystem ;
[System.IO.Compression.ZipFile]::ExtractToDirectory("$HOME\Downloads\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\actions-runner-win-x64-<RUNNER_VERSION>.zip", "$PWD")
```
## OSX

View File

@@ -3,8 +3,6 @@
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="dotnet-core" value="https://www.myget.org/F/dotnet-core/api/v3/index.json" />
<add key="dotnet-buildtools" value="https://www.myget.org/F/dotnet-buildtools/api/v3/index.json" />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

View File

@@ -190,7 +190,7 @@ namespace GitHub.Runner.Listener
{
return GetArgOrPrompt(
name: Constants.Runner.CommandLine.Args.Token,
description: "Enter runner deletion token:",
description: "Enter runner remove token:",
defaultValue: string.Empty,
validator: Validators.NonEmptyValidator);
}
@@ -291,7 +291,7 @@ namespace GitHub.Runner.Listener
if (!string.IsNullOrEmpty(result))
{
// After read the arg from input commandline args, remove it from Arg dictionary,
// This will help if bad arg value passed through CommandLine arg, when ConfigurationManager ask CommandSetting the second time,
// This will help if bad arg value passed through CommandLine arg, when ConfigurationManager ask CommandSetting the second time,
// It will prompt for input instead of continue use the bad input.
_trace.Info($"Remove {name} from Arg dictionary.");
RemoveArg(name);

View File

@@ -1,19 +1,18 @@
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Services.Common;
using GitHub.Services.OAuth;
using GitHub.Services.WebApi;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace GitHub.Runner.Listener.Configuration
{
@@ -277,12 +276,15 @@ namespace GitHub.Runner.Listener.Configuration
throw new NotSupportedException("Message queue listen OAuth token.");
}
// Testing agent connection, detect any protential connection issue, like local clock skew that cause OAuth token expired.
// Testing agent connection, detect any potential connection issue, like local clock skew that cause OAuth token expired.
var credMgr = HostContext.GetService<ICredentialManager>();
VssCredentials credential = credMgr.LoadCredentials();
try
{
await _runnerServer.ConnectAsync(new Uri(runnerSettings.ServerUrl), credential);
// ConnectAsync() hits _apis/connectionData which is an anonymous endpoint
// Need to hit an authenticate endpoint to trigger OAuth token exchange.
await _runnerServer.GetAgentPoolsAsync();
_term.WriteSuccessMessage("Runner connection is good");
}
catch (VssOAuthTokenRequestException ex) when (ex.Message.Contains("Current server time is"))
@@ -519,15 +521,20 @@ namespace GitHub.Runner.Listener.Configuration
private async Task<GitHubAuthResult> GetTenantCredential(string githubUrl, string githubToken)
{
var gitHubUrl = new UriBuilder(githubUrl);
var githubApiUrl = $"https://api.{gitHubUrl.Host}/repos/{gitHubUrl.Path.Trim('/')}/actions-runners/registration";
var gitHubUrlBuilder = new UriBuilder(githubUrl);
var githubApiUrl = $"{gitHubUrlBuilder.Scheme}://api.{gitHubUrlBuilder.Host}/actions/runner-registration";
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("RemoteAuth", githubToken);
httpClient.DefaultRequestHeaders.UserAgent.Add(HostContext.UserAgent);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.shuri-preview+json"));
var response = await httpClient.PostAsync(githubApiUrl, new StringContent("", null, "application/json"));
var bodyObject = new Dictionary<string, string>()
{
{"url", githubUrl}
};
var response = await httpClient.PostAsync(githubApiUrl, new StringContent(StringUtil.ConvertToJson(bodyObject), null, "application/json"));
if (response.IsSuccessStatusCode)
{

View File

@@ -21,6 +21,7 @@ namespace GitHub.Runner.Sdk
private string _httpsProxyAddress;
private string _httpsProxyUsername;
private string _httpsProxyPassword;
private string _noProxyString;
private readonly List<ByPassInfo> _noProxyList = new List<ByPassInfo>();
private readonly HashSet<string> _noProxyUnique = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -33,6 +34,7 @@ namespace GitHub.Runner.Sdk
public string HttpsProxyAddress => _httpsProxyAddress;
public string HttpsProxyUsername => _httpsProxyUsername;
public string HttpsProxyPassword => _httpsProxyPassword;
public string NoProxyString => _noProxyString;
public List<ByPassInfo> NoProxyList => _noProxyList;
@@ -72,8 +74,8 @@ namespace GitHub.Runner.Sdk
_httpProxyAddress = proxyHttpUri.AbsoluteUri;
// Set both environment variables since there are tools support both casing (curl, wget) and tools support only one casing (docker)
Environment.SetEnvironmentVariable("http_proxy", _httpProxyAddress);
Environment.SetEnvironmentVariable("HTTP_PROXY", _httpProxyAddress);
Environment.SetEnvironmentVariable("http_proxy", _httpProxyAddress);
// the proxy url looks like http://[user:pass@]127.0.0.1:8888
var userInfo = Uri.UnescapeDataString(proxyHttpUri.UserInfo).Split(':', 2, StringSplitOptions.RemoveEmptyEntries);
@@ -102,8 +104,8 @@ namespace GitHub.Runner.Sdk
_httpsProxyAddress = proxyHttpsUri.AbsoluteUri;
// Set both environment variables since there are tools support both casing (curl, wget) and tools support only one casing (docker)
Environment.SetEnvironmentVariable("https_proxy", _httpsProxyAddress);
Environment.SetEnvironmentVariable("HTTPS_PROXY", _httpsProxyAddress);
Environment.SetEnvironmentVariable("https_proxy", _httpsProxyAddress);
// the proxy url looks like http://[user:pass@]127.0.0.1:8888
var userInfo = Uri.UnescapeDataString(proxyHttpsUri.UserInfo).Split(':', 2, StringSplitOptions.RemoveEmptyEntries);
@@ -129,9 +131,11 @@ namespace GitHub.Runner.Sdk
if (!string.IsNullOrEmpty(noProxyList))
{
_noProxyString = noProxyList;
// Set both environment variables since there are tools support both casing (curl, wget) and tools support only one casing (docker)
Environment.SetEnvironmentVariable("no_proxy", noProxyList);
Environment.SetEnvironmentVariable("NO_PROXY", noProxyList);
Environment.SetEnvironmentVariable("no_proxy", noProxyList);
var noProxyListSplit = noProxyList.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (string noProxy in noProxyListSplit)

View File

@@ -1,6 +1,7 @@
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Worker.Container;
using System;
using System.Collections.Generic;
using System.IO;
@@ -15,14 +16,14 @@ namespace GitHub.Runner.Worker
{
void EnablePluginInternalCommand();
void DisablePluginInternalCommand();
bool TryProcessCommand(IExecutionContext context, string input);
bool TryProcessCommand(IExecutionContext context, string input, ContainerInfo container);
}
public sealed class ActionCommandManager : RunnerService, IActionCommandManager
{
private const string _stopCommand = "stop-commands";
private readonly Dictionary<string, IActionCommandExtension> _commandExtensions = new Dictionary<string, IActionCommandExtension>(StringComparer.OrdinalIgnoreCase);
private HashSet<string> _registeredCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _registeredCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly object _commandSerializeLock = new object();
private bool _stopProcessCommand = false;
private string _stopToken = null;
@@ -58,7 +59,7 @@ namespace GitHub.Runner.Worker
_registeredCommands.Remove("internal-set-repo-path");
}
public bool TryProcessCommand(IExecutionContext context, string input)
public bool TryProcessCommand(IExecutionContext context, string input, ContainerInfo container)
{
if (string.IsNullOrEmpty(input))
{
@@ -114,7 +115,7 @@ namespace GitHub.Runner.Worker
try
{
extension.ProcessCommand(context, input, actionCommand);
extension.ProcessCommand(context, input, actionCommand, container);
}
catch (Exception ex)
{
@@ -140,7 +141,7 @@ namespace GitHub.Runner.Worker
string Command { get; }
bool OmitEcho { get; }
void ProcessCommand(IExecutionContext context, string line, ActionCommand command);
void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container);
}
public sealed class InternalPluginSetRepoPathCommandExtension : RunnerService, IActionCommandExtension
@@ -150,7 +151,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
if (!command.Properties.TryGetValue(SetRepoPathCommandProperties.repoFullName, out string repoFullName) || string.IsNullOrEmpty(repoFullName))
{
@@ -180,7 +181,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
if (!command.Properties.TryGetValue(SetEnvCommandProperties.Name, out string envName) || string.IsNullOrEmpty(envName))
{
@@ -205,7 +206,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
{
@@ -229,7 +230,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
{
@@ -253,7 +254,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
if (string.IsNullOrWhiteSpace(command.Data))
{
@@ -279,7 +280,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
ArgUtil.NotNullOrEmpty(command.Data, "path");
context.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
@@ -294,7 +295,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
var file = command.Data;
@@ -306,9 +307,9 @@ namespace GitHub.Runner.Worker
}
// Translate file path back from container path
if (context.Container != null)
if (container != null)
{
file = context.Container.TranslateToHostPath(file);
file = container.TranslateToHostPath(file);
}
// Root the path
@@ -341,7 +342,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
command.Properties.TryGetValue(RemoveMatcherCommandProperties.Owner, out string owner);
var file = command.Data;
@@ -369,9 +370,9 @@ namespace GitHub.Runner.Worker
else
{
// Translate file path back from container path
if (context.Container != null)
if (container != null)
{
file = context.Container.TranslateToHostPath(file);
file = container.TranslateToHostPath(file);
}
// Root the path
@@ -409,7 +410,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command, ContainerInfo container)
{
context.Debug(command.Data);
}
@@ -437,7 +438,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command, ContainerInfo container)
{
command.Properties.TryGetValue(IssueCommandProperties.File, out string file);
command.Properties.TryGetValue(IssueCommandProperties.Line, out string line);
@@ -454,10 +455,10 @@ namespace GitHub.Runner.Worker
{
issue.Category = "Code";
if (context.Container != null)
if (container != null)
{
// Translate file path back from container path
file = context.Container.TranslateToHostPath(file);
file = container.TranslateToHostPath(file);
command.Properties[IssueCommandProperties.File] = file;
}
@@ -517,7 +518,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
var data = this is GroupCommandExtension ? command.Data : string.Empty;
context.Output($"##[{Command}]{data}");
@@ -531,7 +532,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
ArgUtil.NotNullOrEmpty(command.Data, "value");

View File

@@ -179,17 +179,15 @@ namespace GitHub.Runner.Worker
ExecutionContext.Debug("Loading env");
var environment = new Dictionary<String, String>(VarUtil.EnvironmentVariableKeyComparer);
// Apply environment set using ##[set-env] first since these are job level env
foreach (var env in ExecutionContext.EnvironmentVariables)
#if OS_WINDOWS
var envContext = ExecutionContext.ExpressionValues["env"] as DictionaryContextData;
#else
var envContext = ExecutionContext.ExpressionValues["env"] as CaseSensitiveDictionaryContextData;
#endif
// Apply environment from env context, env context contains job level env and action's evn block
foreach (var env in envContext)
{
environment[env.Key] = env.Value ?? string.Empty;
}
// Apply action's env block later.
var actionEnvironment = templateEvaluator.EvaluateStepEnvironment(Action.Environment, ExecutionContext.ExpressionValues, VarUtil.EnvironmentVariableKeyComparer);
foreach (var env in actionEnvironment)
{
environment[env.Key] = env.Value ?? string.Empty;
environment[env.Key] = env.Value.ToString();
}
// Apply action's intra-action state at last

View File

@@ -63,6 +63,8 @@ namespace GitHub.Runner.Worker.Container
UserMountVolumes[volume] = volume;
}
}
UpdateWebProxyEnv(hostContext.WebProxy);
}
public string ContainerId { get; set; }
@@ -222,6 +224,26 @@ namespace GitHub.Runner.Worker.Container
{
_pathMappings.Insert(0, new PathMapping(hostCommonPath, containerCommonPath));
}
private void UpdateWebProxyEnv(RunnerWebProxy webProxy)
{
// Set common forms of proxy variables if configured in Runner and not set directly by container.env
if (!String.IsNullOrEmpty(webProxy.HttpProxyAddress))
{
ContainerEnvironmentVariables.TryAdd("HTTP_PROXY", webProxy.HttpProxyAddress);
ContainerEnvironmentVariables.TryAdd("http_proxy", webProxy.HttpProxyAddress);
}
if (!String.IsNullOrEmpty(webProxy.HttpsProxyAddress))
{
ContainerEnvironmentVariables.TryAdd("HTTPS_PROXY", webProxy.HttpsProxyAddress);
ContainerEnvironmentVariables.TryAdd("https_proxy", webProxy.HttpsProxyAddress);
}
if (!String.IsNullOrEmpty(webProxy.NoProxyString))
{
ContainerEnvironmentVariables.TryAdd("NO_PROXY", webProxy.NoProxyString);
ContainerEnvironmentVariables.TryAdd("no_proxy", webProxy.NoProxyString);
}
}
}
public class MountVolume

View File

@@ -84,7 +84,7 @@ namespace GitHub.Runner.Worker.Handlers
{
// This does not need to be inside of a critical section.
// The logging queues and command handlers are thread-safe.
if (_commandManager.TryProcessCommand(_executionContext, line))
if (_commandManager.TryProcessCommand(_executionContext, line, _container))
{
return;
}

View File

@@ -76,15 +76,38 @@ namespace GitHub.Runner.Worker
// Start
step.ExecutionContext.Start();
// Set GITHUB_ACTION
if (step is IActionRunner actionStep)
{
step.ExecutionContext.SetGitHubContext("action", actionStep.Action.Name);
}
// Initialize scope
if (InitializeScope(step, scopeInputs))
{
// Populate env context for each step
Trace.Info("Initialize Env context for step");
#if OS_WINDOWS
var envContext = new DictionaryContextData();
#else
var envContext = new CaseSensitiveDictionaryContextData();
#endif
step.ExecutionContext.ExpressionValues["env"] = envContext;
foreach (var pair in step.ExecutionContext.EnvironmentVariables)
{
envContext[pair.Key] = new StringContextData(pair.Value ?? string.Empty);
}
if (step is IActionRunner actionStep)
{
// Set GITHUB_ACTION
step.ExecutionContext.SetGitHubContext("action", actionStep.Action.Name);
// Evaluate and merge action's env block to env context
var templateTrace = step.ExecutionContext.ToTemplateTraceWriter();
var schema = new PipelineTemplateSchemaFactory().CreateSchema();
var templateEvaluator = new PipelineTemplateEvaluator(templateTrace, schema);
var actionEnvironment = templateEvaluator.EvaluateStepEnvironment(actionStep.Action.Environment, step.ExecutionContext.ExpressionValues, VarUtil.EnvironmentVariableKeyComparer);
foreach (var env in actionEnvironment)
{
envContext[env.Key] = new StringContextData(env.Value ?? string.Empty);
}
}
var expressionManager = HostContext.GetService<IExpressionManager>();
try
{

View File

@@ -162,8 +162,8 @@ namespace GitHub.Services.Common
}
IssuedToken token = null;
IssuedTokenProvider provider = null;
if (this.Credentials != null && this.Credentials.TryGetTokenProvider(request.RequestUri, out provider))
IssuedTokenProvider provider;
if (this.Credentials.TryGetTokenProvider(request.RequestUri, out provider))
{
token = provider.CurrentToken;
}
@@ -227,7 +227,7 @@ namespace GitHub.Services.Common
responseWrapper = new HttpResponseMessageWrapper(response);
if (this.Credentials != null && !this.Credentials.IsAuthenticationChallenge(responseWrapper))
if (!this.Credentials.IsAuthenticationChallenge(responseWrapper))
{
// Validate the token after it has been successfully authenticated with the server.
if (provider != null)
@@ -259,10 +259,7 @@ namespace GitHub.Services.Common
}
// Ensure we have an appropriate token provider for the current challenge
if (this.Credentials != null)
{
provider = this.Credentials.CreateTokenProvider(request.RequestUri, responseWrapper, token);
}
provider = this.Credentials.CreateTokenProvider(request.RequestUri, responseWrapper, token);
// Make sure we don't invoke the provider in an invalid state
if (provider == null)
@@ -311,7 +308,7 @@ namespace GitHub.Services.Common
// We're out of retries and the response was an auth challenge -- then the request was unauthorized
// and we will throw a strongly-typed exception with a friendly error message.
if (!succeeded && response != null && (this.Credentials != null && this.Credentials.IsAuthenticationChallenge(responseWrapper)))
if (!succeeded && response != null && this.Credentials.IsAuthenticationChallenge(responseWrapper))
{
String message = null;
IEnumerable<String> serviceError;

View File

@@ -35,6 +35,19 @@ namespace GitHub.DistributedTask.Pipelines.ContextData
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(DictionaryContextData)}' was expected.");
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static CaseSensitiveDictionaryContextData AssertCaseSensitiveDictionary(
this PipelineContextData value,
String objectDescription)
{
if (value is CaseSensitiveDictionaryContextData dictionary)
{
return dictionary;
}
throw new ArgumentException($"Unexpected type '{value?.GetType().Name}' encountered while reading '{objectDescription}'. The type '{nameof(CaseSensitiveDictionaryContextData)}' was expected.");
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static StringContextData AssertString(
this PipelineContextData value,

View File

@@ -189,11 +189,5 @@ namespace GitHub.Services.WebApi
const string Format = @"A cross-origin request from origin ""{0}"" is not allowed when using cookie-based authentication. An authentication token needs to be provided in the Authorization header of the request.";
return string.Format(CultureInfo.CurrentCulture, Format, arg0);
}
public static string UnknownEntityType(object arg0)
{
const string Format = @"Unknown entityType {0}. Cannot parse.";
return string.Format(CultureInfo.CurrentCulture, Format, arg0);
}
}
}

View File

@@ -1,32 +0,0 @@
using System;
using System.Runtime.Serialization;
using GitHub.Services.WebApi;
using GitHub.Services.WebApi.Jwt;
namespace GitHub.Services.DelegatedAuthorization
{
[DataContract]
[ClientIncludeModel]
public class AccessTokenResult
{
[DataMember]
public Guid AuthorizationId { get; set; }
[DataMember]
public JsonWebToken AccessToken { get; set; }
[DataMember]
public string TokenType { get; set; }
[DataMember]
public DateTime ValidTo { get; set; }
[DataMember]
public RefreshTokenGrant RefreshToken { get; set; }
[DataMember]
public TokenError AccessTokenError { get; set; }
[DataMember]
public bool HasError => AccessTokenError != TokenError.None;
[DataMember]
public string ErrorDescription { get; set; }
}
}

View File

@@ -1,26 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Runtime.Serialization;
namespace GitHub.Services.DelegatedAuthorization
{
[KnownType(typeof(RefreshTokenGrant))]
[KnownType(typeof(JwtBearerAuthorizationGrant))]
[JsonConverter(typeof(AuthorizationGrantJsonConverter))]
public abstract class AuthorizationGrant
{
public AuthorizationGrant(GrantType grantType)
{
if (grantType == GrantType.None)
{
throw new ArgumentException("Grant type is required.");
}
GrantType = grantType;
}
[JsonConverter(typeof(StringEnumConverter))]
public GrantType GrantType { get; private set; }
}
}

View File

@@ -1,50 +0,0 @@
using GitHub.Services.WebApi;
using GitHub.Services.WebApi.Jwt;
using Newtonsoft.Json.Linq;
using System;
namespace GitHub.Services.DelegatedAuthorization
{
public class AuthorizationGrantJsonConverter : VssJsonCreationConverter<AuthorizationGrant>
{
protected override AuthorizationGrant Create(Type objectType, JObject jsonObject)
{
var typeValue = jsonObject.GetValue(nameof(AuthorizationGrant.GrantType), StringComparison.OrdinalIgnoreCase);
if (typeValue == null)
{
throw new ArgumentException(WebApiResources.UnknownEntityType(typeValue));
}
GrantType grantType;
if (typeValue.Type == JTokenType.Integer)
{
grantType = (GrantType)(Int32)typeValue;
}
else if (typeValue.Type != JTokenType.String || !Enum.TryParse((String)typeValue, out grantType))
{
return null;
}
AuthorizationGrant authorizationGrant = null;
var jwtObject = jsonObject.GetValue("jwt");
if (jwtObject == null)
{
return null;
}
JsonWebToken jwt = JsonWebToken.Create(jwtObject.ToString());
switch (grantType)
{
case GrantType.JwtBearer:
authorizationGrant = new JwtBearerAuthorizationGrant(jwt);
break;
case GrantType.RefreshToken:
authorizationGrant = new RefreshTokenGrant(jwt);
break;
}
return authorizationGrant;
}
}
}

View File

@@ -1,11 +0,0 @@
namespace GitHub.Services.DelegatedAuthorization
{
public enum GrantType
{
None = 0,
JwtBearer = 1,
RefreshToken = 2,
Implicit = 3,
ClientCredentials = 4,
}
}

View File

@@ -1,20 +0,0 @@
using GitHub.Services.WebApi.Jwt;
namespace GitHub.Services.DelegatedAuthorization
{
public class JwtBearerAuthorizationGrant : AuthorizationGrant
{
public JwtBearerAuthorizationGrant(JsonWebToken jwt)
: base(GrantType.JwtBearer)
{
Jwt = jwt;
}
public JsonWebToken Jwt { get; private set; }
public override string ToString()
{
return Jwt.EncodedToken;
}
}
}

View File

@@ -1,20 +0,0 @@
using GitHub.Services.WebApi.Jwt;
namespace GitHub.Services.DelegatedAuthorization
{
public class RefreshTokenGrant : AuthorizationGrant
{
public RefreshTokenGrant(JsonWebToken jwt)
: base(GrantType.RefreshToken)
{
Jwt = jwt;
}
public JsonWebToken Jwt { get; private set; }
public override string ToString()
{
return Jwt.EncodedToken;
}
}
}

View File

@@ -1,39 +0,0 @@
namespace GitHub.Services.DelegatedAuthorization
{
public enum TokenError
{
None,
GrantTypeRequired,
AuthorizationGrantRequired,
ClientSecretRequired,
RedirectUriRequired,
InvalidAuthorizationGrant,
InvalidAuthorizationScopes,
InvalidRefreshToken,
AuthorizationNotFound,
AuthorizationGrantExpired,
AccessAlreadyIssued,
InvalidRedirectUri,
AccessTokenNotFound,
InvalidAccessToken,
AccessTokenAlreadyRefreshed,
InvalidClientSecret,
ClientSecretExpired,
ServerError,
AccessDenied,
AccessTokenKeyRequired,
InvalidAccessTokenKey,
FailedToGetAccessToken,
InvalidClientId,
InvalidClient,
InvalidValidTo,
InvalidUserId,
FailedToIssueAccessToken,
AuthorizationGrantScopeMissing,
InvalidPublicAccessTokenKey,
InvalidPublicAccessToken,
/* Deprecated */
PublicFeatureFlagNotEnabled,
SSHPolicyDisabled
}
}

View File

@@ -1,8 +0,0 @@
namespace GitHub.Services.Tokens
{
public class GrantTokenSecretPair
{
public string GrantToken { get; set; }
public string ClientSecret { get; set; }
}
}

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Services.Common;
using GitHub.Services.DelegatedAuthorization;
using GitHub.Services.WebApi;
namespace GitHub.Services.Tokens.WebApi
{
[ResourceArea(TokenOAuth2ResourceIds.AreaId)]
public class TokenOauth2HttpClient : VssHttpClientBase
{
public TokenOauth2HttpClient(Uri baseUrl, VssCredentials credentials)
: base(baseUrl, credentials)
{
}
public TokenOauth2HttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings)
: base(baseUrl, credentials, settings)
{
}
public TokenOauth2HttpClient(Uri baseUrl, VssCredentials credentials, params DelegatingHandler[] handlers)
: base(baseUrl, credentials, handlers)
{
}
public TokenOauth2HttpClient(Uri baseUrl, VssCredentials credentials, VssHttpRequestSettings settings, params DelegatingHandler[] handlers)
: base(baseUrl, credentials, settings, handlers)
{
}
public TokenOauth2HttpClient(Uri baseUrl, HttpMessageHandler pipeline, bool disposeHandler)
: base(baseUrl, pipeline, disposeHandler)
{
}
/// <summary>
/// [Preview API]
/// </summary>
/// <param name="tokenSecretPair"></param>
/// <param name="grantType"></param>
/// <param name="hostId"></param>
/// <param name="orgHostId"></param>
/// <param name="audience"></param>
/// <param name="redirectUri"></param>
/// <param name="accessId"></param>
/// <param name="userState"></param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
public Task<AccessTokenResult> IssueTokenAsync(
GrantTokenSecretPair tokenSecretPair,
GrantType grantType,
Guid hostId,
Guid orgHostId,
Uri audience = null,
Uri redirectUri = null,
Guid? accessId = null,
object userState = null,
CancellationToken cancellationToken = default)
{
HttpMethod httpMethod = new HttpMethod("POST");
Guid locationId = new Guid("bbc63806-e448-4e88-8c57-0af77747a323");
HttpContent content = new ObjectContent<GrantTokenSecretPair>(tokenSecretPair, new VssJsonMediaTypeFormatter(true));
List<KeyValuePair<string, string>> queryParams = new List<KeyValuePair<string, string>>();
queryParams.Add("grantType", grantType.ToString());
queryParams.Add("hostId", hostId.ToString());
queryParams.Add("orgHostId", orgHostId.ToString());
if (audience != null)
{
queryParams.Add("audience", audience.ToString());
}
if (redirectUri != null)
{
queryParams.Add("redirectUri", redirectUri.ToString());
}
if (accessId != null)
{
queryParams.Add("accessId", accessId.Value.ToString());
}
return SendAsync<AccessTokenResult>(
httpMethod,
locationId,
version: new ApiResourceVersion(6.0, 1),
queryParameters: queryParams,
userState: userState,
cancellationToken: cancellationToken,
content: content);
}
}
}

View File

@@ -8,7 +8,6 @@ using System.Threading;
using System.Threading.Tasks;
using GitHub.Services.Common;
using GitHub.Services.Common.Diagnostics;
using GitHub.Services.Tokens;
using GitHub.Services.WebApi;
namespace GitHub.Services.OAuth
@@ -56,69 +55,45 @@ namespace GitHub.Services.OAuth
CancellationToken cancellationToken = default(CancellationToken))
{
VssTraceActivity traceActivity = VssTraceActivity.Current;
using (var tokenClient = new Tokens.WebApi.TokenOauth2HttpClient(new Uri("https://vstoken.actions.githubusercontent.com"), null, CreateMessageHandler(this.AuthorizationUrl)))
using (HttpClient client = new HttpClient(CreateMessageHandler(this.AuthorizationUrl)))
{
var parameters = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
(credential as IVssOAuthTokenParameterProvider).SetParameters(parameters);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, this.AuthorizationUrl);
requestMessage.Content = CreateRequestContent(grant, credential, tokenParameters);
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
GrantTokenSecretPair tokenSecretPair = new GrantTokenSecretPair()
if (VssClientHttpRequestSettings.Default.UseHttp11)
{
ClientSecret = parameters[VssOAuthConstants.ClientAssertion],
GrantToken = null
};
requestMessage.Version = HttpVersion.Version11;
}
var hostId = new Guid("bf08a85e-7241-4858-aeb8-ac70056a16d4");
var tokenResult = await tokenClient.IssueTokenAsync(tokenSecretPair, DelegatedAuthorization.GrantType.ClientCredentials, hostId, hostId, cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var headerVal in VssClientHttpRequestSettings.Default.UserAgent)
{
if (!requestMessage.Headers.UserAgent.Contains(headerVal))
{
requestMessage.Headers.UserAgent.Add(headerVal);
}
}
var response = new VssOAuthTokenResponse();
response.AccessToken = tokenResult.AccessToken.EncodedToken;
response.Error = tokenResult.AccessTokenError.ToString();
response.ErrorDescription = tokenResult.ErrorDescription;
response.RefreshToken = tokenResult.RefreshToken?.Jwt?.EncodedToken;
response.Scope = tokenResult.AccessToken.Scopes;
response.TokenType = tokenResult.TokenType;
return response;
using (var response = await client.SendAsync(requestMessage, cancellationToken: cancellationToken).ConfigureAwait(false))
{
string correlationId = "Unknown";
if (response.Headers.TryGetValues("x-ms-request-id", out IEnumerable<string> requestIds))
{
correlationId = string.Join(",", requestIds);
}
VssHttpEventSource.Log.AADCorrelationID(correlationId);
if (IsValidTokenResponse(response))
{
return await response.Content.ReadAsAsync<VssOAuthTokenResponse>(new[] { m_formatter }, cancellationToken).ConfigureAwait(false);
}
else
{
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new VssServiceResponseException(response.StatusCode, responseContent, null);
}
}
}
// using (HttpClient client = new HttpClient(CreateMessageHandler(this.AuthorizationUrl)))
// {
// var requestMessage = new HttpRequestMessage(HttpMethod.Post, this.AuthorizationUrl);
// requestMessage.Content = CreateRequestContent(grant, credential, tokenParameters);
// requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// if (VssClientHttpRequestSettings.Default.UseHttp11)
// {
// requestMessage.Version = HttpVersion.Version11;
// }
// foreach (var headerVal in VssClientHttpRequestSettings.Default.UserAgent)
// {
// if (!requestMessage.Headers.UserAgent.Contains(headerVal))
// {
// requestMessage.Headers.UserAgent.Add(headerVal);
// }
// }
// using (var response = await client.SendAsync(requestMessage, cancellationToken: cancellationToken).ConfigureAwait(false))
// {
// string correlationId = "Unknown";
// if (response.Headers.TryGetValues("x-ms-request-id", out IEnumerable<string> requestIds))
// {
// correlationId = string.Join(",", requestIds);
// }
// VssHttpEventSource.Log.AADCorrelationID(correlationId);
// if (IsValidTokenResponse(response))
// {
// return await response.Content.ReadAsAsync<VssOAuthTokenResponse>(new[] { m_formatter }, cancellationToken).ConfigureAwait(false);
// }
// else
// {
// var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// throw new VssServiceResponseException(response.StatusCode, responseContent, null);
// }
// }
// }
}
private static Boolean IsValidTokenResponse(HttpResponseMessage response)
@@ -126,7 +101,7 @@ namespace GitHub.Services.OAuth
return response.StatusCode == HttpStatusCode.OK || (response.StatusCode == HttpStatusCode.BadRequest && IsJsonResponse(response));
}
private static DelegatingHandler CreateMessageHandler(Uri requestUri)
private static HttpMessageHandler CreateMessageHandler(Uri requestUri)
{
var retryOptions = new VssHttpRetryOptions()
{
@@ -137,7 +112,33 @@ namespace GitHub.Services.OAuth
},
};
return new VssHttpRetryMessageHandler(retryOptions);
HttpClientHandler messageHandler = new HttpClientHandler()
{
UseDefaultCredentials = false
};
// Inherit proxy setting from VssHttpMessageHandler
if (VssHttpMessageHandler.DefaultWebProxy != null)
{
messageHandler.Proxy = VssHttpMessageHandler.DefaultWebProxy;
messageHandler.UseProxy = true;
}
if (requestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
VssClientHttpRequestSettings.Default.ClientCertificateManager != null &&
VssClientHttpRequestSettings.Default.ClientCertificateManager.ClientCertificates != null &&
VssClientHttpRequestSettings.Default.ClientCertificateManager.ClientCertificates.Count > 0)
{
messageHandler.ClientCertificates.AddRange(VssClientHttpRequestSettings.Default.ClientCertificateManager.ClientCertificates);
}
if (requestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) &&
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback != null)
{
messageHandler.ServerCertificateCustomValidationCallback = VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback;
}
return new VssHttpRetryMessageHandler(retryOptions, messageHandler);
}
private static HttpContent CreateRequestContent(params IVssOAuthTokenParameterProvider[] parameterProviders)

View File

@@ -40,17 +40,4 @@ namespace GitHub.Services.Location
public static readonly Guid SpsServiceDefinition = new Guid("{DF5F298A-4E06-4815-A13E-6CE90A37EFA4}");
}
}
namespace GitHub.Services.Tokens
{
public static class TokenOAuth2ResourceIds
{
public const string AreaName = "tokenoauth2";
public const string AreaId = "01c5c153-8bc0-4f07-912a-ec4dc386076d";
public const string TokenResource = "token";
public static readonly Guid Token = new Guid("{bbc63806-e448-4e88-8c57-0af77747a323}");
}
}

View File

@@ -498,7 +498,7 @@ namespace GitHub.Runner.Common.Tests
_promptManager
.Setup(x => x.ReadValue(
Constants.Runner.CommandLine.Args.Token, // argName
"Enter runner deletion token:", // description
"Enter runner remove token:", // description
true, // secret
string.Empty, // defaultValue
Validators.NonEmptyValidator, // validator

View File

@@ -175,8 +175,8 @@ namespace GitHub.Runner.Common.Tests.Listener.Configuration
Assert.True(s.PoolId.Equals(_expectedPoolId));
Assert.True(s.WorkFolder.Equals(_expectedWorkFolder));
// validate GetAgentPoolsAsync gets called once with automation pool type
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Once);
// validate GetAgentPoolsAsync gets called twice with automation pool type
_runnerServer.Verify(x => x.GetAgentPoolsAsync(It.IsAny<string>(), It.Is<TaskAgentPoolType>(p => p == TaskAgentPoolType.Automation)), Times.Exactly(2));
_runnerServer.Verify(x => x.AddAgentAsync(It.IsAny<int>(), It.Is<TaskAgent>(a => a.Labels.Contains("self-hosted") && a.Labels.Contains(VarUtil.OS) && a.Labels.Contains(VarUtil.OSArchitecture))), Times.Once);
}

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using Moq;
using Xunit;
using Pipelines = GitHub.DistributedTask.Pipelines;
@@ -11,47 +13,35 @@ namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class ActionCommandManagerL0
{
private ActionCommandManager _commandManager;
private Mock<IExecutionContext> _ec;
private Mock<IExtensionManager> _extensionManager;
private Mock<IPipelineDirectoryManager> _pipelineDirectoryManager;
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnablePluginInternalCommand()
{
using (TestHostContext _hc = new TestHostContext(this))
using (TestHostContext hc = CreateTestContext())
{
var extensionManger = new Mock<IExtensionManager>();
var directoryManager = new Mock<IPipelineDirectoryManager>();
var pluginCommand = new InternalPluginSetRepoPathCommandExtension();
pluginCommand.Initialize(_hc);
var envCommand = new SetEnvCommandExtension();
envCommand.Initialize(_hc);
extensionManger.Setup(x => x.GetExtensions<IActionCommandExtension>())
.Returns(new List<IActionCommandExtension>() { pluginCommand, envCommand });
_hc.SetSingleton<IExtensionManager>(extensionManger.Object);
_hc.SetSingleton<IPipelineDirectoryManager>(directoryManager.Object);
Mock<IExecutionContext> _ec = new Mock<IExecutionContext>();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Returns((string tag, string line) =>
{
_hc.GetTrace().Info($"{tag} {line}");
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
{
_hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
});
ActionCommandManager commandManager = new ActionCommandManager();
commandManager.Initialize(_hc);
commandManager.EnablePluginInternalCommand();
_commandManager.EnablePluginInternalCommand();
Assert.True(commandManager.TryProcessCommand(_ec.Object, "##[internal-set-repo-path repoFullName=actions/runner;workspaceRepo=true]somepath"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "##[internal-set-repo-path repoFullName=actions/runner;workspaceRepo=true]somepath", null));
directoryManager.Verify(x => x.UpdateRepositoryDirectory(_ec.Object, "actions/runner", "somepath", true), Times.Once);
_pipelineDirectoryManager.Verify(x => x.UpdateRepositoryDirectory(_ec.Object, "actions/runner", "somepath", true), Times.Once);
}
}
@@ -60,47 +50,29 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void DisablePluginInternalCommand()
{
using (TestHostContext _hc = new TestHostContext(this))
using (TestHostContext hc = CreateTestContext())
{
var extensionManger = new Mock<IExtensionManager>();
var directoryManager = new Mock<IPipelineDirectoryManager>();
var pluginCommand = new InternalPluginSetRepoPathCommandExtension();
pluginCommand.Initialize(_hc);
var envCommand = new SetEnvCommandExtension();
envCommand.Initialize(_hc);
extensionManger.Setup(x => x.GetExtensions<IActionCommandExtension>())
.Returns(new List<IActionCommandExtension>() { pluginCommand, envCommand });
_hc.SetSingleton<IExtensionManager>(extensionManger.Object);
_hc.SetSingleton<IPipelineDirectoryManager>(directoryManager.Object);
Mock<IExecutionContext> _ec = new Mock<IExecutionContext>();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Returns((string tag, string line) =>
{
_hc.GetTrace().Info($"{tag} {line}");
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
{
_hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
});
ActionCommandManager commandManager = new ActionCommandManager();
commandManager.Initialize(_hc);
commandManager.EnablePluginInternalCommand();
_commandManager.EnablePluginInternalCommand();
Assert.True(commandManager.TryProcessCommand(_ec.Object, "##[internal-set-repo-path repoFullName=actions/runner;workspaceRepo=true]somepath"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "##[internal-set-repo-path repoFullName=actions/runner;workspaceRepo=true]somepath", null));
commandManager.DisablePluginInternalCommand();
_commandManager.DisablePluginInternalCommand();
Assert.False(commandManager.TryProcessCommand(_ec.Object, "##[internal-set-repo-path repoFullName=actions/runner;workspaceRepo=true]somepath"));
Assert.False(_commandManager.TryProcessCommand(_ec.Object, "##[internal-set-repo-path repoFullName=actions/runner;workspaceRepo=true]somepath", null));
directoryManager.Verify(x => x.UpdateRepositoryDirectory(_ec.Object, "actions/runner", "somepath", true), Times.Once);
_pipelineDirectoryManager.Verify(x => x.UpdateRepositoryDirectory(_ec.Object, "actions/runner", "somepath", true), Times.Once);
}
}
@@ -109,42 +81,27 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void StopProcessCommand()
{
using (TestHostContext _hc = new TestHostContext(this))
using (TestHostContext hc = CreateTestContext())
{
var extensionManger = new Mock<IExtensionManager>();
var pluginCommand = new InternalPluginSetRepoPathCommandExtension();
pluginCommand.Initialize(_hc);
var envCommand = new SetEnvCommandExtension();
envCommand.Initialize(_hc);
extensionManger.Setup(x => x.GetExtensions<IActionCommandExtension>())
.Returns(new List<IActionCommandExtension>() { pluginCommand, envCommand });
_hc.SetSingleton<IExtensionManager>(extensionManger.Object);
Mock<IExecutionContext> _ec = new Mock<IExecutionContext>();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Returns((string tag, string line) =>
{
_hc.GetTrace().Info($"{tag} {line}");
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
{
_hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
});
_ec.Setup(x => x.EnvironmentVariables).Returns(new Dictionary<string, string>());
ActionCommandManager commandManager = new ActionCommandManager();
commandManager.Initialize(_hc);
Assert.True(commandManager.TryProcessCommand(_ec.Object, "##[stop-commands]stopToken"));
Assert.False(commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar"));
Assert.True(commandManager.TryProcessCommand(_ec.Object, "##[stopToken]"));
Assert.True(commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "##[stop-commands]stopToken", null));
Assert.False(_commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar", null));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "##[stopToken]", null));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar", null));
}
}
@@ -153,41 +110,29 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void EchoProcessCommand()
{
using (TestHostContext _hc = new TestHostContext(this))
using (TestHostContext hc = CreateTestContext())
{
var extensionManager = new Mock<IExtensionManager>();
var echoCommand = new EchoCommandExtension();
echoCommand.Initialize(_hc);
extensionManager.Setup(x => x.GetExtensions<IActionCommandExtension>())
.Returns(new List<IActionCommandExtension>() { echoCommand });
_hc.SetSingleton<IExtensionManager>(extensionManager.Object);
Mock<IExecutionContext> _ec = new Mock<IExecutionContext>();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Returns((string tag, string line) =>
{
_hc.GetTrace().Info($"{tag} {line}");
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.SetupAllProperties();
ActionCommandManager commandManager = new ActionCommandManager();
commandManager.Initialize(_hc);
Assert.False(_ec.Object.EchoOnActionCommand);
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::on"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::echo::on", null));
Assert.True(_ec.Object.EchoOnActionCommand);
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::off"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::echo::off", null));
Assert.False(_ec.Object.EchoOnActionCommand);
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::ON"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::echo::ON", null));
Assert.True(_ec.Object.EchoOnActionCommand);
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::Off "));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::echo::Off ", null));
Assert.False(_ec.Object.EchoOnActionCommand);
}
}
@@ -197,7 +142,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void EchoProcessCommandDebugOn()
{
using (TestHostContext _hc = new TestHostContext(this))
using (TestHostContext hc = CreateTestContext())
{
// Set up a few things
// 1. Job request message (with ACTIONS_STEP_DEBUG = true)
@@ -219,84 +164,135 @@ namespace GitHub.Runner.Common.Tests.Worker
var jobServerQueue = new Mock<IJobServerQueue>();
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
_hc.SetSingleton(jobServerQueue.Object);
var extensionManager = new Mock<IExtensionManager>();
var echoCommand = new EchoCommandExtension();
echoCommand.Initialize(_hc);
extensionManager.Setup(x => x.GetExtensions<IActionCommandExtension>())
.Returns(new List<IActionCommandExtension>() { echoCommand });
_hc.SetSingleton<IExtensionManager>(extensionManager.Object);
hc.SetSingleton(jobServerQueue.Object);
var configurationStore = new Mock<IConfigurationStore>();
configurationStore.Setup(x => x.GetSettings()).Returns(new RunnerSettings());
_hc.SetSingleton(configurationStore.Object);
hc.SetSingleton(configurationStore.Object);
var pagingLogger = new Mock<IPagingLogger>();
_hc.EnqueueInstance(pagingLogger.Object);
ActionCommandManager commandManager = new ActionCommandManager();
commandManager.Initialize(_hc);
var _ec = new Runner.Worker.ExecutionContext();
_ec.Initialize(_hc);
hc.EnqueueInstance(pagingLogger.Object);
// Initialize the job (to exercise logic that sets EchoOnActionCommand)
_ec.InitializeJob(jobRequest, System.Threading.CancellationToken.None);
var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
ec.InitializeJob(jobRequest, System.Threading.CancellationToken.None);
_ec.Complete();
ec.Complete();
Assert.True(_ec.EchoOnActionCommand);
Assert.True(ec.EchoOnActionCommand);
Assert.True(commandManager.TryProcessCommand(_ec, "::echo::off"));
Assert.False(_ec.EchoOnActionCommand);
Assert.True(_commandManager.TryProcessCommand(ec, "::echo::off", null));
Assert.False(ec.EchoOnActionCommand);
Assert.True(commandManager.TryProcessCommand(_ec, "::echo::on"));
Assert.True(_ec.EchoOnActionCommand);
Assert.True(_commandManager.TryProcessCommand(ec, "::echo::on", null));
Assert.True(ec.EchoOnActionCommand);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EchoProcessCommandInvalid()
{
using (TestHostContext _hc = new TestHostContext(this))
using (TestHostContext hc = CreateTestContext())
{
var extensionManager = new Mock<IExtensionManager>();
var echoCommand = new EchoCommandExtension();
echoCommand.Initialize(_hc);
extensionManager.Setup(x => x.GetExtensions<IActionCommandExtension>())
.Returns(new List<IActionCommandExtension>() { echoCommand });
_hc.SetSingleton<IExtensionManager>(extensionManager.Object);
Mock<IExecutionContext> _ec = new Mock<IExecutionContext>();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Returns((string tag, string line) =>
{
_hc.GetTrace().Info($"{tag} {line}");
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.SetupAllProperties();
ActionCommandManager commandManager = new ActionCommandManager();
commandManager.Initialize(_hc);
// Echo commands below are considered "processed", but are invalid
// 1. Invalid echo value
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::invalid"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::echo::invalid", null));
Assert.Equal(TaskResult.Failed, _ec.Object.CommandResult);
Assert.False(_ec.Object.EchoOnActionCommand);
// 2. No value
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::"));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::echo::", null));
Assert.Equal(TaskResult.Failed, _ec.Object.CommandResult);
Assert.False(_ec.Object.EchoOnActionCommand);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void AddMatcherTranslatesFilePath()
{
using (TestHostContext hc = CreateTestContext())
{
// Create a problem matcher config file
var hostDirectory = hc.GetDirectory(WellKnownDirectory.Temp);
var hostFile = Path.Combine(hostDirectory, "my-matcher.json");
Directory.CreateDirectory(hostDirectory);
var content = @"
{
""problemMatcher"": [
{
""owner"": ""my-matcher"",
""pattern"": [
{
""regexp"": ""^ERROR: (.+)$"",
""message"": 1
}
]
}
]
}";
File.WriteAllText(hostFile, content);
// Setup translation info
var container = new ContainerInfo();
var containerDirectory = "/some-container-directory";
var containerFile = Path.Combine(containerDirectory, "my-matcher.json");
container.AddPathTranslateMapping(hostDirectory, containerDirectory);
// Act
_commandManager.TryProcessCommand(_ec.Object, $"::add-matcher::{containerFile}", container);
// Assert
_ec.Verify(x => x.AddMatchers(It.IsAny<IssueMatchersConfig>()), Times.Once);
}
}
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
{
var hostContext = new TestHostContext(this, testName);
// Mock extension manager
_extensionManager = new Mock<IExtensionManager>();
var commands = new IActionCommandExtension[]
{
new AddMatcherCommandExtension(),
new EchoCommandExtension(),
new InternalPluginSetRepoPathCommandExtension(),
new SetEnvCommandExtension(),
};
foreach (var command in commands)
{
command.Initialize(hostContext);
}
_extensionManager.Setup(x => x.GetExtensions<IActionCommandExtension>())
.Returns(new List<IActionCommandExtension>(commands));
hostContext.SetSingleton<IExtensionManager>(_extensionManager.Object);
// Mock pipeline directory manager
_pipelineDirectoryManager = new Mock<IPipelineDirectoryManager>();
hostContext.SetSingleton<IPipelineDirectoryManager>(_pipelineDirectoryManager.Object);
// Execution context
_ec = new Mock<IExecutionContext>();
// Command manager
_commandManager = new ActionCommandManager();
_commandManager.Initialize(hostContext);
return hostContext;
}
}
}

View File

@@ -314,6 +314,12 @@ namespace GitHub.Runner.Common.Tests.Worker
githubContext.Add("event", JToken.Parse("{\"foo\":\"bar\"}").ToPipelineContextData());
_context.Add("github", githubContext);
#if OS_WINDOWS
_context["env"] = new DictionaryContextData();
#else
_context["env"] = new CaseSensitiveDictionaryContextData();
#endif
_ec = new Mock<IExecutionContext>();
_ec.Setup(x => x.ExpressionValues).Returns(_context);
_ec.Setup(x => x.IntraActionState).Returns(new Dictionary<string, string>());

View File

@@ -973,8 +973,8 @@ namespace GitHub.Runner.Common.Tests.Worker
});
_commandManager = new Mock<IActionCommandManager>();
_commandManager.Setup(x => x.TryProcessCommand(It.IsAny<IExecutionContext>(), It.IsAny<string>()))
.Returns((IExecutionContext executionContext, string line) =>
_commandManager.Setup(x => x.TryProcessCommand(It.IsAny<IExecutionContext>(), It.IsAny<string>(), It.IsAny<ContainerInfo>()))
.Returns((IExecutionContext executionContext, string line, ContainerInfo container) =>
{
if (line.IndexOf("##[some-command]") >= 0)
{

View File

@@ -19,6 +19,7 @@ namespace GitHub.Runner.Common.Tests.Worker
private Mock<IExecutionContext> _ec;
private StepsRunner _stepsRunner;
private Variables _variables;
private Dictionary<string, string> _env;
private DictionaryContextData _contexts;
private JobContext _jobContext;
private StepsContext _stepContext;
@@ -32,6 +33,11 @@ namespace GitHub.Runner.Common.Tests.Worker
_variables = new Variables(
hostContext: hc,
copy: variablesToCopy);
_env = new Dictionary<string, string>()
{
{"env1", "1"},
{"test", "github_actions"}
};
_ec = new Mock<IExecutionContext>();
_ec.SetupAllProperties();
_ec.Setup(x => x.Variables).Returns(_variables);
@@ -64,9 +70,9 @@ namespace GitHub.Runner.Common.Tests.Worker
// Arrange.
var variableSets = new[]
{
new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Succeeded, "success()") },
new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Succeeded, "success() || failure()") },
new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Succeeded, "always()") }
new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Succeeded, "success()") },
new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Succeeded, "success() || failure()") },
new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") }
};
foreach (var variableSet in variableSets)
{
@@ -96,12 +102,12 @@ namespace GitHub.Runner.Common.Tests.Worker
// Arrange.
var variableSets = new[]
{
new[] { CreateStep(TaskResult.Failed, "success()", true), CreateStep(TaskResult.Succeeded, "success()") },
new[] { CreateStep(TaskResult.Failed, "success()", true), CreateStep(TaskResult.Succeeded, "success() || failure()") },
new[] { CreateStep(TaskResult.Failed, "success()", true), CreateStep(TaskResult.Succeeded, "always()") },
new[] { CreateStep(TaskResult.Failed, "success()", true), CreateStep(TaskResult.Failed, "success()", true) },
new[] { CreateStep(TaskResult.Failed, "success()", true), CreateStep(TaskResult.Failed, "success() || failure()", true) },
new[] { CreateStep(TaskResult.Failed, "success()", true), CreateStep(TaskResult.Failed, "always()", true) }
new[] { CreateStep(hc, TaskResult.Failed, "success()", true), CreateStep(hc, TaskResult.Succeeded, "success()") },
new[] { CreateStep(hc, TaskResult.Failed, "success()", true), CreateStep(hc, TaskResult.Succeeded, "success() || failure()") },
new[] { CreateStep(hc, TaskResult.Failed, "success()", true), CreateStep(hc, TaskResult.Succeeded, "always()") },
new[] { CreateStep(hc, TaskResult.Failed, "success()", true), CreateStep(hc, TaskResult.Failed, "success()", true) },
new[] { CreateStep(hc, TaskResult.Failed, "success()", true), CreateStep(hc, TaskResult.Failed, "success() || failure()", true) },
new[] { CreateStep(hc, TaskResult.Failed, "success()", true), CreateStep(hc, TaskResult.Failed, "always()", true) }
};
foreach (var variableSet in variableSets)
{
@@ -133,12 +139,12 @@ namespace GitHub.Runner.Common.Tests.Worker
{
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "success()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "success()") },
Expected = false,
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "success() || failure()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "success() || failure()") },
Expected = true,
},
};
@@ -172,27 +178,27 @@ namespace GitHub.Runner.Common.Tests.Worker
{
new
{
Steps = new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Succeeded, "always()") },
Steps = new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") },
Expected = TaskResult.Succeeded,
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "always()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") },
Expected = TaskResult.Failed,
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "always()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") },
Expected = TaskResult.Failed,
},
new
{
Steps = new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Failed, "always()") },
Steps = new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Failed, "always()") },
Expected = TaskResult.Failed,
},
new
{
Steps = new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Failed, "always()", true) },
Steps = new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Failed, "always()", true) },
Expected = TaskResult.Succeeded,
},
};
@@ -226,47 +232,47 @@ namespace GitHub.Runner.Common.Tests.Worker
{
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "success()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "success()") },
Expected = TaskResult.Failed
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "success() || failure()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "success() || failure()") },
Expected = TaskResult.Failed
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "always()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") },
Expected = TaskResult.Failed
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()", continueOnError: true), CreateStep(TaskResult.Failed, "success()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()", continueOnError: true), CreateStep(hc, TaskResult.Failed, "success()") },
Expected = TaskResult.Failed
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()", continueOnError: true), CreateStep(TaskResult.Succeeded, "success()") },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()", continueOnError: true), CreateStep(hc, TaskResult.Succeeded, "success()") },
Expected = TaskResult.Succeeded
},
new
{
Steps = new[] { CreateStep(TaskResult.Failed, "success()", continueOnError: true), CreateStep(TaskResult.Failed, "success()", continueOnError: true) },
Steps = new[] { CreateStep(hc, TaskResult.Failed, "success()", continueOnError: true), CreateStep(hc, TaskResult.Failed, "success()", continueOnError: true) },
Expected = TaskResult.Succeeded
},
new
{
Steps = new[] { CreateStep(TaskResult.Succeeded, "success() || failure()") },
Steps = new[] { CreateStep(hc, TaskResult.Succeeded, "success() || failure()") },
Expected = TaskResult.Succeeded
},
new
{
Steps = new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Failed, "success()") },
Steps = new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Failed, "success()") },
Expected = TaskResult.Failed
},
new
{
Steps = new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Succeeded, "success()") },
Steps = new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Succeeded, "success()") },
Expected = TaskResult.Succeeded
},
// Abandoned
@@ -304,17 +310,17 @@ namespace GitHub.Runner.Common.Tests.Worker
{
new
{
Step = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "success()") },
Step = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "success()") },
Expected = false
},
new
{
Step = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "success() || failure()") },
Step = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "success() || failure()") },
Expected = true
},
new
{
Step = new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "always()") },
Step = new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") },
Expected = true
}
};
@@ -345,9 +351,9 @@ namespace GitHub.Runner.Common.Tests.Worker
// Arrange.
var variableSets = new[]
{
new[] { CreateStep(TaskResult.Succeeded, "success()"), CreateStep(TaskResult.Succeeded, "always()") },
new[] { CreateStep(TaskResult.Failed, "success()"), CreateStep(TaskResult.Succeeded, "always()") },
new[] { CreateStep(TaskResult.Canceled, "success()"), CreateStep(TaskResult.Succeeded, "always()") }
new[] { CreateStep(hc, TaskResult.Succeeded, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") },
new[] { CreateStep(hc, TaskResult.Failed, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") },
new[] { CreateStep(hc, TaskResult.Canceled, "success()"), CreateStep(hc, TaskResult.Succeeded, "always()") }
};
foreach (var variableSet in variableSets)
{
@@ -381,8 +387,8 @@ namespace GitHub.Runner.Common.Tests.Worker
// Arrange.
var variableSets = new[]
{
new[] { CreateStep(TaskResult.Succeeded, "success()") },
new[] { CreateStep(TaskResult.Succeeded, "success()") },
new[] { CreateStep(hc, TaskResult.Succeeded, "success()") },
new[] { CreateStep(hc, TaskResult.Succeeded, "success()") },
};
foreach (var variableSet in variableSets)
{
@@ -399,18 +405,134 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
private Mock<IStep> CreateStep(TaskResult result, string condition, Boolean continueOnError = false)
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StepEnvOverrideJobEnvContext()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var env1 = new MappingToken(null, null, null);
env1.Add(new StringToken(null, null, null, "env1"), new StringToken(null, null, null, "100"));
env1.Add(new StringToken(null, null, null, "env2"), new BasicExpressionToken(null, null, null, "env.test"));
var step1 = CreateStep(hc, TaskResult.Succeeded, "success()", env: env1);
_ec.Object.Result = null;
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new[] { step1.Object }));
// Act.
await _stepsRunner.RunAsync(jobContext: _ec.Object);
// Assert.
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
#if OS_WINDOWS
Assert.Equal("100", _ec.Object.ExpressionValues["env"].AssertDictionary("env")["env1"].AssertString("100"));
Assert.Equal("github_actions", _ec.Object.ExpressionValues["env"].AssertDictionary("env")["env2"].AssertString("github_actions"));
#else
Assert.Equal("100", _ec.Object.ExpressionValues["env"].AssertCaseSensitiveDictionary("env")["env1"].AssertString("100"));
Assert.Equal("github_actions", _ec.Object.ExpressionValues["env"].AssertCaseSensitiveDictionary("env")["env2"].AssertString("github_actions"));
#endif
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task PopulateEnvContextForEachStep()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var env1 = new MappingToken(null, null, null);
env1.Add(new StringToken(null, null, null, "env1"), new StringToken(null, null, null, "100"));
env1.Add(new StringToken(null, null, null, "env2"), new BasicExpressionToken(null, null, null, "env.test"));
var step1 = CreateStep(hc, TaskResult.Succeeded, "success()", env: env1);
var env2 = new MappingToken(null, null, null);
env2.Add(new StringToken(null, null, null, "env1"), new StringToken(null, null, null, "1000"));
env2.Add(new StringToken(null, null, null, "env3"), new BasicExpressionToken(null, null, null, "env.test"));
var step2 = CreateStep(hc, TaskResult.Succeeded, "success()", env: env2);
_ec.Object.Result = null;
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new[] { step1.Object, step2.Object }));
// Act.
await _stepsRunner.RunAsync(jobContext: _ec.Object);
// Assert.
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
#if OS_WINDOWS
Assert.Equal("1000", _ec.Object.ExpressionValues["env"].AssertDictionary("env")["env1"].AssertString("1000"));
Assert.Equal("github_actions", _ec.Object.ExpressionValues["env"].AssertDictionary("env")["env3"].AssertString("github_actions"));
Assert.False(_ec.Object.ExpressionValues["env"].AssertDictionary("env").ContainsKey("env2"));
#else
Assert.Equal("1000", _ec.Object.ExpressionValues["env"].AssertCaseSensitiveDictionary("env")["env1"].AssertString("1000"));
Assert.Equal("github_actions", _ec.Object.ExpressionValues["env"].AssertCaseSensitiveDictionary("env")["env3"].AssertString("github_actions"));
Assert.False(_ec.Object.ExpressionValues["env"].AssertCaseSensitiveDictionary("env").ContainsKey("env2"));
#endif
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task PopulateEnvContextAfterSetupStepsContext()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var env1 = new MappingToken(null, null, null);
env1.Add(new StringToken(null, null, null, "env1"), new StringToken(null, null, null, "100"));
var step1 = CreateStep(hc, TaskResult.Succeeded, "success()", env: env1, name: "foo", setOutput: true);
var env2 = new MappingToken(null, null, null);
env2.Add(new StringToken(null, null, null, "env1"), new StringToken(null, null, null, "1000"));
env2.Add(new StringToken(null, null, null, "env2"), new BasicExpressionToken(null, null, null, "steps.foo.outputs.test"));
var step2 = CreateStep(hc, TaskResult.Succeeded, "success()", env: env2);
_ec.Object.Result = null;
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new[] { step1.Object, step2.Object }));
// Act.
await _stepsRunner.RunAsync(jobContext: _ec.Object);
// Assert.
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
#if OS_WINDOWS
Assert.Equal("1000", _ec.Object.ExpressionValues["env"].AssertDictionary("env")["env1"].AssertString("1000"));
Assert.Equal("something", _ec.Object.ExpressionValues["env"].AssertDictionary("env")["env2"].AssertString("something"));
#else
Assert.Equal("1000", _ec.Object.ExpressionValues["env"].AssertCaseSensitiveDictionary("env")["env1"].AssertString("1000"));
Assert.Equal("something", _ec.Object.ExpressionValues["env"].AssertCaseSensitiveDictionary("env")["env2"].AssertString("something"));
#endif
}
}
private Mock<IActionRunner> CreateStep(TestHostContext hc, TaskResult result, string condition, Boolean continueOnError = false, MappingToken env = null, string name = "Test", bool setOutput = false)
{
// Setup the step.
var step = new Mock<IStep>();
var step = new Mock<IActionRunner>();
step.Setup(x => x.Condition).Returns(condition);
step.Setup(x => x.ContinueOnError).Returns(new BooleanToken(null, null, null, continueOnError));
step.Setup(x => x.RunAsync()).Returns(Task.CompletedTask);
step.Setup(x => x.Action)
.Returns(new DistributedTask.Pipelines.ActionStep()
{
Name = name,
Id = Guid.NewGuid(),
Environment = env
});
// Setup the step execution context.
var stepContext = new Mock<IExecutionContext>();
stepContext.SetupAllProperties();
stepContext.Setup(x => x.WriteDebug).Returns(true);
stepContext.Setup(x => x.Variables).Returns(_variables);
stepContext.Setup(x => x.EnvironmentVariables).Returns(_env);
stepContext.Setup(x => x.ExpressionValues).Returns(_contexts);
stepContext.Setup(x => x.JobContext).Returns(_jobContext);
stepContext.Setup(x => x.StepsContext).Returns(_stepContext);
@@ -422,13 +544,24 @@ namespace GitHub.Runner.Common.Tests.Worker
stepContext.Object.Result = r;
}
});
var trace = hc.GetTrace();
stepContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { trace.Info($"[{tag}]{message}"); });
stepContext.Object.Result = result;
step.Setup(x => x.ExecutionContext).Returns(stepContext.Object);
if (setOutput)
{
step.Setup(x => x.RunAsync()).Callback(() => { _stepContext.SetOutput(null, name, "test", "something", out string reference); }).Returns(Task.CompletedTask);
}
else
{
step.Setup(x => x.RunAsync()).Returns(Task.CompletedTask);
}
return step;
}
private string FormatSteps(IEnumerable<Mock<IStep>> steps)
private string FormatSteps(IEnumerable<Mock<IActionRunner>> steps)
{
return String.Join(
" ; ",

View File

@@ -1,5 +1,16 @@
@setlocal
@echo off
rem add expected utils to path
IF EXIST C:\Program Files\Git\usr\bin (
SET PATH=C:\Program Files\Git\usr\bin;%PATH%
)
IF EXIST C:\Program Files\Git\mingw64\bin (
SET PATH=C:\Program Files\Git\mingw64\bin;%PATH%
)
IF EXIST C:\Program Files\Git\bin (
SET PATH=C:\Program Files\Git\bin;%PATH%
)
rem Check if SH_PATH is defined.
if defined SH_PATH (
goto run

View File

@@ -1 +1 @@
2.164.0
2.165.2