Compare commits

...

10 Commits

Author SHA1 Message Date
Thomas Boop
436989949b Update releaseVersion 2022-09-20 14:23:02 -04:00
Thomas Boop
06f7d8a4a3 fix workflows (#2143) 2022-09-20 14:02:36 -04:00
Thomas Boop
06e1773933 M283 Hotfix for container env escaping (#2138)
* Fix escaping of docker envs backport

* create as prerelease

* update release notes

Co-authored-by: Nikola Jokic <97525037+nikola-jokic@users.noreply.github.com>
2022-09-20 12:53:33 -04:00
Thomas Boop
844595b1b3 Update releaseVersion 2021-10-04 15:10:53 -04:00
Thomas Boop
98a1935e50 fix ephemeral runner upgrade on mac/linux (#1401) 2021-10-04 14:30:09 -04:00
Ferenc Hammerl
4d908df4b2 Create new release 2.283.2 2021-09-30 14:58:13 +02:00
Ferenc Hammerl
b53b520498 Create Release 2.283.2 (#1394)
* Update telemetry (#1355)

* Track "pause-logging"

* Bump release version

* Don't retry 422 (#1352)

* Update the comment about the `--once` in Constants.cs (#1360)

* Update Constants.cs

* feedback.

* Update src/Runner.Listener/Runner.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Fix unconfiguring of runner after group changes (#1359)

* Ignore agentpool when unconfiguring the runner

Runner names and IDs are unique within a ServiceHost
They don't need to be included when unconfiguring the runner.

* Use -1 instead of 0 to highlight how it is ignored

* Use overloads and 0 instead of -1

Using 0 seems to be the convention

* Fix typo calling the wrong method

* only cleanup runner local files on success (#1384)

* Stop-Commands: stopToken restrictions (#1371)

* Prevent stopTokens that are workflow commands

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Check context for env var too

* Accept true, 1 and $true instead of just "true"

* Setup ExpressionValues in tests

* Update src/Runner.Common/Constants.cs

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Separate success and fail tests for invalid token

* Fix envcontext for tests

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>

* Handle keeping previous OSX versions more smoothly on Mac (#1381)

* Handle macOS upgrade smoothly

* cleanup

* misc cleanup

* final updates

* Update src/Misc/layoutbin/update.sh.template

Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>

* Update src/Misc/layoutbin/update.sh.template

Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>

* Upload telemetry and default to old method as needed

* minor fix

* add one more bit of logging

* some more telemetry

* quote variables to handle spaces

* tiny fix for ubuntu

* remove version and move telemetry to diag

* use full path

Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>

* Runner 2.283.2 Release (#1389)

Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
Co-authored-by: Tingluo Huang <tingluohuang@github.com>
Co-authored-by: Patrick Ellis <319655+pje@users.noreply.github.com>
2021-09-30 14:53:01 +02:00
Ferenc Hammerl
e6baf0d275 Release 2.283.1 2021-09-20 16:02:11 +02:00
Ferenc Hammerl
e7ad4652b6 [Backport] Update telemetry (#1356)
* Track "pause-logging"

* Bump release version
2021-09-20 15:54:24 +02:00
Ferenc Hammerl
b03ca604ff Release 2.283.0 2021-09-20 15:15:16 +02:00
21 changed files with 436 additions and 72 deletions

View File

@@ -37,7 +37,7 @@ jobs:
devScript: ./dev.sh
- runtime: win-x64
os: windows-latest
os: windows-2019
devScript: ./dev
runs-on: ${{ matrix.os }}

View File

@@ -72,7 +72,7 @@ jobs:
devScript: ./dev.sh
- runtime: win-x64
os: windows-latest
os: windows-2019
devScript: ./dev
runs-on: ${{ matrix.os }}
@@ -164,6 +164,7 @@ jobs:
release_name: "v${{ steps.releaseNote.outputs.version }}"
body: |
${{ steps.releaseNote.outputs.note }}
prerelease: true
# Upload release assets
- name: Upload Release Asset (win-x64)

View File

@@ -1,14 +1,11 @@
## Features
- Collect more telemetry
- Make `runner.name` available as a runner context variable
- Add attempt number (`run_attempt`) to GitHub context
- When using the `--ephemeral` flag, ensure that the runner cleans up local `.runner` and `.credentials` files after completion (#1337)
## Bugs
- Fixed an issue where container environment variables names or values could escape the docker command (#2108)
## Misc
- Improved network troubleshooting docs
## Windows x64
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.

View File

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

View File

@@ -18,6 +18,8 @@ downloadrunnerversion=_DOWNLOAD_RUNNER_VERSION_
logfile="_UPDATE_LOG_"
restartinteractiverunner=_RESTART_INTERACTIVE_RUNNER_
telemetryfile="$rootfolder/_diag/.telemetry"
# log user who run the script
date "+[%F %T-%4N] --------whoami--------" >> "$logfile" 2>&1
whoami >> "$logfile" 2>&1
@@ -118,40 +120,101 @@ then
exit 1
fi
# fix upgrade issue with macOS
# fix upgrade issue with macOS when running as a service
attemptedtargetedfix=0
currentplatform=$(uname | awk '{print tolower($0)}')
if [[ "$currentplatform" == 'darwin' ]]; then
# need a short-term fix for https://github.com/actions/runner/issues/743
# we will recreate all the ./externals/node12/bin/node of the past 5 versions
# v2.280.3 v2.280.2 v2.280.1 v2.279.0 v2.278.0
if [[ ! -e "$rootfolder/externals.2.280.3/node12/bin/node" ]]
if [[ "$currentplatform" == 'darwin' && restartinteractiverunner -eq 0 ]]; then
# We needed a fix for https://github.com/actions/runner/issues/743
# We will recreate the ./externals/node12/bin/node of the past runner version that launched the runnerlistener service
# Otherwise mac gatekeeper kills the processes we spawn on creation as we are running a process with no backing file
# We need the pid for the nodejs loop, get that here, its the parent of the runner C# pid
# assumption here is only one process is invoking rootfolder/runsvc.sh
procgroup=$(ps x -o pgid,command | grep "$rootfolder/runsvc.sh" | grep -v grep | awk '{print $1}')
if [[ $? -eq 0 && -n "$procgroup" ]]
then
mkdir -p "$rootfolder/externals.2.280.3/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.280.3/node12/bin/node"
# inspect the open file handles to find the node process
# we can't actually inspect the process using ps because it uses relative paths and doesn't follow symlinks
path=$(lsof -a -g "$procgroup" -F n | grep node12/bin/node | grep externals | tail -1 | cut -c2-)
if [[ $? -eq 0 && -n "$path" ]]
then
# trim the last 5 characters of the path '/node'
trimmedpath=$(dirname "$path")
if [[ $? -eq 0 && -n "$trimmedpath" ]]
then
attemptedtargetedfix=1
# Create the path if it does not exist
if [[ ! -e "$path" ]]
then
date "+[%F %T-%4N] Creating fallback node at path $path" >> "$logfile" 2>&1
mkdir -p "$trimmedpath"
cp "$rootfolder/externals/node12/bin/node" "$path"
else
date "+[%F %T-%4N] Path for fallback node exists, skipping creating $path" >> "$logfile" 2>&1
fi
else
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to trim runner path. TrimmedPath: $trimmedpath, path: $path, pgid: $procgroup, root: $rootfolder" >> "$logfile" 2>&1
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to trim runner path. TrimmedPath: $trimmedpath, path: $path, pgid: $procgroup, root: $rootfolder" >> "$telemetryfile" 2>&1
fi
else
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner path. Path: $path, pgid: $procgroup, root: $rootfolder" >> "$logfile" 2>&1
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner path. Path: $path, pgid: $procgroup, root: $rootfolder" >> "$telemetryfile" 2>&1
fi
else
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner pgid. pgid: $procgroup, root: $rootfolder" >> "$logfile" 2>&1
date "+[%F %T-%4N] DarwinRunnerUpgrade: Failed to find runner pgid. pgid: $procgroup, root: $rootfolder" >> "$telemetryfile" 2>&1
fi
if [[ ! -e "$rootfolder/externals.2.280.2/node12/bin/node" ]]
if [ $attemptedtargetedfix -eq 0 ]
then
mkdir -p "$rootfolder/externals.2.280.2/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.280.2/node12/bin/node"
fi
if [[ ! -e "$rootfolder/externals.2.280.1/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.280.1/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.280.1/node12/bin/node"
fi
date "+[%F %T-%4N] DarwinRunnerUpgrade: Defaulting to old macOS service fix" >> "$logfile" 2>&1
date "+[%F %T-%4N] DarwinRunnerUpgrade: Defaulting to old macOS service fix" >> "$telemetryfile" 2>&1
if [[ ! -e "$rootfolder/externals.2.280.3/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.280.3/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.280.3/node12/bin/node"
fi
if [[ ! -e "$rootfolder/externals.2.279.0/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.279.0/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.279.0/node12/bin/node"
fi
if [[ ! -e "$rootfolder/externals.2.280.2/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.280.2/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.280.2/node12/bin/node"
fi
if [[ ! -e "$rootfolder/externals.2.278.0/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.278.0/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.278.0/node12/bin/node"
if [[ ! -e "$rootfolder/externals.2.280.1/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.280.1/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.280.1/node12/bin/node"
fi
# GHES 3.2
if [[ ! -e "$rootfolder/externals.2.279.0/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.279.0/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.279.0/node12/bin/node"
fi
# GHES 3.1.2 or later
if [[ ! -e "$rootfolder/externals.2.278.0/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.278.0/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.278.0/node12/bin/node"
fi
# GHES 3.1.0
if [[ ! -e "$rootfolder/externals.2.276.1/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.276.1/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.276.1/node12/bin/node"
fi
# GHES 3.0
if [[ ! -e "$rootfolder/externals.2.273.5/node12/bin/node" ]]
then
mkdir -p "$rootfolder/externals.2.273.5/node12/bin"
cp "$rootfolder/externals/node12/bin/node" "$rootfolder/externals.2.273.5/node12/bin/node"
fi
fi
fi

View File

@@ -43,6 +43,21 @@ else
else
sleep 5
fi
elif [[ $returnCode == 4 ]]; then
if [ ! -x "$(command -v sleep)" ]; then
if [ ! -x "$(command -v ping)" ]; then
COUNT="0"
while [[ $COUNT != 5000 ]]; do
echo "SLEEP" > /dev/null
COUNT=$[$COUNT+1]
done
else
ping -c 5 127.0.0.1 > /dev/null
fi
else
sleep 5
fi
"$DIR"/bin/Runner.Listener run $*
else
exit $returnCode
fi

View File

@@ -26,6 +26,7 @@ namespace GitHub.Runner.Common
Certificates,
Options,
SetupInfo,
Telemetry
}
public static class Constants
@@ -128,7 +129,7 @@ namespace GitHub.Runner.Common
public static readonly string Ephemeral = "ephemeral";
public static readonly string Help = "help";
public static readonly string Replace = "replace";
public static readonly string Once = "once"; // TODO: Remove in 10/2021
public static readonly string Once = "once"; // Keep this around since customers still relies on it
public static readonly string RunAsService = "runasservice";
public static readonly string Unattended = "unattended";
public static readonly string Version = "version";
@@ -154,6 +155,7 @@ namespace GitHub.Runner.Common
public static readonly string LowDiskSpace = "LOW_DISK_SPACE";
public static readonly string UnsupportedCommand = "UNSUPPORTED_COMMAND";
public static readonly string UnsupportedCommandMessageDisabled = "The `{0}` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/";
public static readonly string UnsupportedStopCommandTokenDisabled = "You cannot use a endToken that is an empty string, the string 'pause-logging', or another workflow command. For more information see: https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#example-stopping-and-starting-workflow-commands or opt into insecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS` environment variable to `true`.";
}
public static class RunnerEvent
@@ -213,6 +215,7 @@ namespace GitHub.Runner.Common
// Keep alphabetical
//
public static readonly string AllowUnsupportedCommands = "ACTIONS_ALLOW_UNSECURE_COMMANDS";
public static readonly string AllowUnsupportedStopCommandTokens = "ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS";
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
}

View File

@@ -342,6 +342,12 @@ namespace GitHub.Runner.Common
GetDirectory(WellKnownDirectory.Root),
".setup_info");
break;
case WellKnownConfigFile.Telemetry:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Diag),
".telemetry");
break;
default:
throw new NotSupportedException($"Unexpected well known config file: '{configFile}'");

View File

@@ -29,8 +29,10 @@ namespace GitHub.Runner.Common
// Configuration
Task<TaskAgent> AddAgentAsync(Int32 agentPoolId, TaskAgent agent);
Task DeleteAgentAsync(int agentPoolId, int agentId);
Task DeleteAgentAsync(int agentId);
Task<List<TaskAgentPool>> GetAgentPoolsAsync(string agentPoolName = null, TaskAgentPoolType poolType = TaskAgentPoolType.Automation);
Task<List<TaskAgent>> GetAgentsAsync(int agentPoolId, string agentName = null);
Task<List<TaskAgent>> GetAgentsAsync(string agentName);
Task<TaskAgent> ReplaceAgentAsync(int agentPoolId, TaskAgent agent);
// messagequeue
@@ -252,6 +254,11 @@ namespace GitHub.Runner.Common
return _genericTaskAgentClient.GetAgentsAsync(agentPoolId, agentName, false);
}
public Task<List<TaskAgent>> GetAgentsAsync(string agentName)
{
return GetAgentsAsync(0, agentName); // search in all all agentPools
}
public Task<TaskAgent> ReplaceAgentAsync(int agentPoolId, TaskAgent agent)
{
CheckConnection(RunnerConnectionType.Generic);
@@ -264,6 +271,11 @@ namespace GitHub.Runner.Common
return _genericTaskAgentClient.DeleteAgentAsync(agentPoolId, agentId);
}
public Task DeleteAgentAsync(int agentId)
{
return DeleteAgentAsync(0, agentId); // agentPool is ignored server side
}
//-----------------------------------------------------------------
// MessageQueue
//-----------------------------------------------------------------

View File

@@ -31,6 +31,7 @@ namespace GitHub.Runner.Listener
Constants.Runner.CommandLine.Flags.Commit,
Constants.Runner.CommandLine.Flags.Ephemeral,
Constants.Runner.CommandLine.Flags.Help,
Constants.Runner.CommandLine.Flags.Once,
Constants.Runner.CommandLine.Flags.Replace,
Constants.Runner.CommandLine.Flags.RunAsService,
Constants.Runner.CommandLine.Flags.Unattended,
@@ -68,7 +69,7 @@ namespace GitHub.Runner.Listener
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral);
// TODO: Remove in 10/2021
// Keep this around since customers still relies on it
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
// Constructor.

View File

@@ -415,7 +415,7 @@ namespace GitHub.Runner.Listener.Configuration
// Determine the service deployment type based on connection data. (Hosted/OnPremises)
await _runnerServer.ConnectAsync(new Uri(settings.ServerUrl), creds);
var agents = await _runnerServer.GetAgentsAsync(settings.PoolId, settings.AgentName);
var agents = await _runnerServer.GetAgentsAsync(settings.AgentName);
Trace.Verbose("Returns {0} agents", agents.Count);
TaskAgent agent = agents.FirstOrDefault();
if (agent == null)
@@ -424,7 +424,7 @@ namespace GitHub.Runner.Listener.Configuration
}
else
{
await _runnerServer.DeleteAgentAsync(settings.PoolId, settings.AgentId);
await _runnerServer.DeleteAgentAsync(settings.AgentId);
_term.WriteLine();
_term.WriteSuccessMessage("Runner removed successfully");

View File

@@ -233,8 +233,14 @@ namespace GitHub.Runner.Listener
Trace.Info($"Set runner startup type - {startType}");
HostContext.StartupType = startType;
if (command.RunOnce)
{
_term.WriteLine("Warning: '--once' is going to be deprecated in the future, please consider using '--ephemeral' during runner registration.", ConsoleColor.Yellow);
_term.WriteLine("https://docs.github.com/en/actions/hosting-your-own-runners/autoscaling-with-self-hosted-runners#using-ephemeral-runners-for-autoscaling", ConsoleColor.Yellow);
}
// Run the runner interactively or as service
return await RunAsync(settings, command.RunOnce || settings.Ephemeral); // TODO: Remove RunOnce later.
return await RunAsync(settings, command.RunOnce || settings.Ephemeral);
}
else
{
@@ -310,6 +316,9 @@ namespace GitHub.Runner.Listener
IJobDispatcher jobDispatcher = null;
CancellationTokenSource messageQueueLoopTokenSource = CancellationTokenSource.CreateLinkedTokenSource(HostContext.RunnerShutdownToken);
// Should we try to cleanup ephemeral runners
bool runOnceJobCompleted = false;
try
{
var notification = HostContext.GetService<IJobNotification>();
@@ -371,6 +380,7 @@ namespace GitHub.Runner.Listener
Task completeTask = await Task.WhenAny(getNextMessage, jobDispatcher.RunOnceJobCompleted.Task);
if (completeTask == jobDispatcher.RunOnceJobCompleted.Task)
{
runOnceJobCompleted = true;
Trace.Info("Job has finished at backend, the runner will exit since it is running under onetime use mode.");
Trace.Info("Stop message queue looping.");
messageQueueLoopTokenSource.Cancel();
@@ -479,7 +489,7 @@ namespace GitHub.Runner.Listener
messageQueueLoopTokenSource.Dispose();
if (settings.Ephemeral)
if (settings.Ephemeral && runOnceJobCompleted)
{
var configManager = HostContext.GetService<IConfigurationManager>();
configManager.DeleteLocalRunnerConfig();

View File

@@ -108,20 +108,18 @@ namespace GitHub.Runner.Worker
// Stop command
if (string.Equals(actionCommand.Command, _stopCommand, StringComparison.OrdinalIgnoreCase))
{
context.Output(input);
context.Debug("Paused processing commands until '##[{actionCommand.Data}]' is received");
ValidateStopToken(context, actionCommand.Data);
_stopToken = actionCommand.Data;
if (_registeredCommands.Contains(actionCommand.Data) || string.IsNullOrEmpty(actionCommand.Data))
{
var telemetry = new JobTelemetry
{
Message = $"Invoked ::stopCommand:: with token: [{actionCommand.Data}]",
Type = JobTelemetryType.ActionCommand
};
context.JobTelemetry.Add(telemetry);
}
_stopProcessCommand = true;
_registeredCommands.Add(_stopToken);
if (_stopToken.Length > 6)
{
HostContext.SecretMasker.AddValue(_stopToken);
}
context.Output(input);
context.Debug("Paused processing commands until the token you called ::stopCommands:: with is received");
return true;
}
// Found command
@@ -155,7 +153,42 @@ namespace GitHub.Runner.Worker
return true;
}
internal static bool EnhancedAnnotationsEnabled(IExecutionContext context) {
private void ValidateStopToken(IExecutionContext context, string stopToken)
{
#if OS_WINDOWS
var envContext = context.ExpressionValues["env"] as DictionaryContextData;
#else
var envContext = context.ExpressionValues["env"] as CaseSensitiveDictionaryContextData;
#endif
var allowUnsecureStopCommandTokens = false;
allowUnsecureStopCommandTokens = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowUnsupportedStopCommandTokens));
if (!allowUnsecureStopCommandTokens && envContext.ContainsKey(Constants.Variables.Actions.AllowUnsupportedStopCommandTokens))
{
allowUnsecureStopCommandTokens = StringUtil.ConvertToBoolean(envContext[Constants.Variables.Actions.AllowUnsupportedStopCommandTokens].ToString());
}
bool isTokenInvalid = _registeredCommands.Contains(stopToken)
|| string.IsNullOrEmpty(stopToken)
|| string.Equals(stopToken, "pause-logging", StringComparison.OrdinalIgnoreCase);
if (isTokenInvalid)
{
var telemetry = new JobTelemetry
{
Message = $"Invoked ::stopCommand:: with token: [{stopToken}]",
Type = JobTelemetryType.ActionCommand
};
context.JobTelemetry.Add(telemetry);
}
if (isTokenInvalid && !allowUnsecureStopCommandTokens)
{
throw new Exception(Constants.Runner.UnsupportedStopCommandTokenDisabled);
}
}
internal static bool EnhancedAnnotationsEnabled(IExecutionContext context)
{
return context.Global.Variables.GetBoolean("DistributedTask.EnhancedAnnotations") ?? false;
}
}
@@ -259,7 +292,7 @@ namespace GitHub.Runner.Worker
public const String Name = "name";
}
private string[] _setEnvBlockList =
private string[] _setEnvBlockList =
{
"NODE_OPTIONS"
};
@@ -360,7 +393,7 @@ namespace GitHub.Runner.Worker
public Type ExtensionType => typeof(IActionCommandExtension);
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container)
{
{
var allowUnsecureCommands = false;
bool.TryParse(Environment.GetEnvironmentVariable(Constants.Variables.Actions.AllowUnsupportedCommands), out allowUnsecureCommands);
@@ -549,11 +582,11 @@ namespace GitHub.Runner.Worker
command.Properties.TryGetValue(IssueCommandProperties.Line, out string line);
command.Properties.TryGetValue(IssueCommandProperties.Column, out string column);
if (!ActionCommandManager.EnhancedAnnotationsEnabled(context))
if (!ActionCommandManager.EnhancedAnnotationsEnabled(context))
{
context.Debug("Enhanced Annotations not enabled on the server. The 'title', 'end_line', and 'end_column' fields are unsupported.");
}
Issue issue = new Issue()
{
Category = "General",
@@ -605,7 +638,7 @@ namespace GitHub.Runner.Worker
context.AddIssue(issue);
}
public static void ValidateLinesAndColumns(ActionCommand command, IExecutionContext context)
public static void ValidateLinesAndColumns(ActionCommand command, IExecutionContext context)
{
command.Properties.TryGetValue(IssueCommandProperties.Line, out string line);
command.Properties.TryGetValue(IssueCommandProperties.EndLine, out string endLine);
@@ -634,28 +667,28 @@ namespace GitHub.Runner.Worker
column = endColumn;
}
if (!hasStartLine && hasColumn)
if (!hasStartLine && hasColumn)
{
context.Debug($"Invalid {command.Command} command value. '{IssueCommandProperties.Column}' and '{IssueCommandProperties.EndColumn}' can only be set if '{IssueCommandProperties.Line}' value is provided.");
command.Properties.Remove(IssueCommandProperties.Column);
command.Properties.Remove(IssueCommandProperties.EndColumn);
}
if (hasEndLine && line != endLine && hasColumn)
if (hasEndLine && line != endLine && hasColumn)
{
context.Debug($"Invalid {command.Command} command value. '{IssueCommandProperties.Column}' and '{IssueCommandProperties.EndColumn}' cannot be set if '{IssueCommandProperties.Line}' and '{IssueCommandProperties.EndLine}' are different values.");
command.Properties.Remove(IssueCommandProperties.Column);
command.Properties.Remove(IssueCommandProperties.EndColumn);
}
if (hasStartLine && hasEndLine && endLineNumber < lineNumber)
if (hasStartLine && hasEndLine && endLineNumber < lineNumber)
{
context.Debug($"Invalid {command.Command} command value. '{IssueCommandProperties.EndLine}' cannot be less than '{IssueCommandProperties.Line}'.");
command.Properties.Remove(IssueCommandProperties.Line);
command.Properties.Remove(IssueCommandProperties.EndLine);
}
if (hasStartColumn && hasEndColumn && endColumnNumber < columnNumber)
if (hasStartColumn && hasEndColumn && endColumnNumber < columnNumber)
{
context.Debug($"Invalid {command.Command} command value. '{IssueCommandProperties.EndColumn}' cannot be less than '{IssueCommandProperties.Column}'.");
command.Properties.Remove(IssueCommandProperties.Column);

View File

@@ -633,7 +633,12 @@ namespace GitHub.Runner.Worker
}
catch (Exception ex) when (!executionContext.CancellationToken.IsCancellationRequested) // Do not retry if the run is canceled.
{
if (attempt < 3)
// UnresolvableActionDownloadInfoException is a 422 client error, don't retry
// Some possible cases are:
// * Repo is rate limited
// * Repo or tag doesn't exist, or isn't public
// * Policy validation failed
if (attempt < 3 && !(ex is WebApi.UnresolvableActionDownloadInfoException))
{
executionContext.Output($"Failed to resolve action download info. Error: {ex.Message}");
executionContext.Debug(ex.ToString());
@@ -649,6 +654,7 @@ namespace GitHub.Runner.Worker
// Some possible cases are:
// * Repo is rate limited
// * Repo or tag doesn't exist, or isn't public
// * Policy validation failed
if (ex is WebApi.UnresolvableActionDownloadInfoException)
{
throw;

View File

@@ -131,11 +131,11 @@ namespace GitHub.Runner.Worker.Container
{
if (String.IsNullOrEmpty(env.Value))
{
dockerOptions.Add($"-e \"{env.Key}\"");
dockerOptions.Add(DockerUtil.CreateEscapedOption("-e", env.Key));
}
else
{
dockerOptions.Add($"-e \"{env.Key}={env.Value.Replace("\"", "\\\"")}\"");
dockerOptions.Add(DockerUtil.CreateEscapedOption("-e", env.Key, env.Value));
}
}
@@ -202,7 +202,7 @@ namespace GitHub.Runner.Worker.Container
{
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
// the value directly in the command
dockerOptions.Add($"-e {env.Key}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("-e", env.Key));
}
// Watermark for GitHub Action environment

View File

@@ -6,6 +6,9 @@ namespace GitHub.Runner.Worker.Container
{
public class DockerUtil
{
private static readonly Regex QuoteEscape = new Regex(@"(\\*)" + "\"", RegexOptions.Compiled);
private static readonly Regex EndOfStringEscape = new Regex(@"(\\+)$", RegexOptions.Compiled);
public static List<PortMapping> ParseDockerPort(IList<string> portMappingLines)
{
const string targetPort = "targetPort";
@@ -17,7 +20,7 @@ namespace GitHub.Runner.Worker.Container
string pattern = $"^(?<{targetPort}>\\d+)/(?<{proto}>\\w+) -> (?<{host}>.+):(?<{hostPort}>\\d+)$";
List<PortMapping> portMappings = new List<PortMapping>();
foreach(var line in portMappingLines)
foreach (var line in portMappingLines)
{
Match m = Regex.Match(line, pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
if (m.Success)
@@ -61,5 +64,44 @@ namespace GitHub.Runner.Worker.Container
}
return "";
}
public static string CreateEscapedOption(string flag, string key)
{
if (String.IsNullOrEmpty(key))
{
return "";
}
return $"{flag} {EscapeString(key)}";
}
public static string CreateEscapedOption(string flag, string key, string value)
{
if (String.IsNullOrEmpty(key))
{
return "";
}
var escapedString = EscapeString($"{key}={value}");
return $"{flag} {escapedString}";
}
private static string EscapeString(string value)
{
if (String.IsNullOrEmpty(value))
{
return "";
}
// Dotnet escaping rules are weird here, we can only escape \ if it precedes a "
// If a double quotation mark follows two or an even number of backslashes, each proceeding backslash pair is replaced with one backslash and the double quotation mark is removed.
// If a double quotation mark follows an odd number of backslashes, including just one, each preceding pair is replaced with one backslash and the remaining backslash is removed; however, in this case the double quotation mark is not removed.
// https://docs.microsoft.com/en-us/dotnet/api/system.environment.getcommandlineargs?redirectedfrom=MSDN&view=net-6.0#remarks
// First, find any \ followed by a " and double the number of \ + 1.
value = QuoteEscape.Replace(value, @"$1$1\" + "\"");
// Next, what if it ends in `\`, it would escape the end quote. So, we need to detect that at the end of the string and perform the same escape
// Luckily, we can just use the $ character with detects the end of string in regex
value = EndOfStringEscape.Replace(value, @"$1$1");
// Finally, wrap it in quotes
return $"\"{value}\"";
}
}
}

View File

@@ -188,7 +188,7 @@ namespace GitHub.Runner.Worker.Handlers
{
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
// the value directly in the command
dockerCommandArgs.Add($"-e {env.Key}");
dockerCommandArgs.Add(DockerUtil.CreateEscapedOption("-e", env.Key));
}
if (!string.IsNullOrEmpty(PrependPath))
{

View File

@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
@@ -228,6 +229,9 @@ namespace GitHub.Runner.Worker
return result;
}
// Load any upgrade telemetry
LoadFromTelemetryFile(jobContext.JobTelemetry);
// Make sure we don't submit secrets as telemetry
MaskTelemetrySecrets(jobContext.JobTelemetry);
@@ -285,6 +289,30 @@ namespace GitHub.Runner.Worker
}
}
private void LoadFromTelemetryFile(List<JobTelemetry> jobTelemetry)
{
try
{
var telemetryFilePath = HostContext.GetConfigFile(WellKnownConfigFile.Telemetry);
if (File.Exists(telemetryFilePath))
{
var telemetryData = File.ReadAllText(telemetryFilePath, Encoding.UTF8);
var telemetry = new JobTelemetry
{
Message = $"Runner File Telemetry:\n{telemetryData}",
Type = JobTelemetryType.General
};
jobTelemetry.Add(telemetry);
IOUtil.DeleteFile(telemetryFilePath);
}
}
catch (Exception e)
{
Trace.Error("Error when trying to load telemetry from telemetry file");
Trace.Error(e);
}
}
private async Task ShutdownQueue(bool throwOnFailure)
{
if (_jobServerQueue != null)

View File

@@ -144,5 +144,54 @@ namespace GitHub.Runner.Common.Tests.Worker.Container
var actual = DockerUtil.ParseRegistryHostnameFromImageName(input);
Assert.Equal(expected, actual);
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("", "")]
[InlineData("foo", "foo")]
[InlineData("foo \\ bar", "foo \\ bar")]
[InlineData("foo \\", "foo \\\\")]
[InlineData("foo \\\\", "foo \\\\\\\\")]
[InlineData("foo \\\" bar", "foo \\\\\\\" bar")]
[InlineData("foo \\\\\" bar", "foo \\\\\\\\\\\" bar")]
public void CreateEscapedOption_keyOnly(string input, string escaped)
{
var flag = "--example";
var actual = DockerUtil.CreateEscapedOption(flag, input);
string expected;
if (String.IsNullOrEmpty(input))
{
expected = "";
}
else
{
expected = $"{flag} \"{escaped}\"";
}
Assert.Equal(expected, actual);
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("foo", "bar", "foo=bar")]
[InlineData("foo\\", "bar", "foo\\=bar")]
[InlineData("foo\\", "bar\\", "foo\\=bar\\\\")]
[InlineData("foo \\","bar \\", "foo \\=bar \\\\")]
public void CreateEscapedOption_keyValue(string keyInput, string valueInput, string escapedString)
{
var flag = "--example";
var actual = DockerUtil.CreateEscapedOption(flag, keyInput, valueInput);
string expected;
if (String.IsNullOrEmpty(keyInput))
{
expected = "";
}
else
{
expected = $"{flag} \"{escapedString}\"";
}
Assert.Equal(expected, actual);
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
@@ -83,6 +84,7 @@ namespace GitHub.Runner.Common.Tests.Worker
{
using (TestHostContext hc = CreateTestContext())
{
_ec.Setup(x => x.ExpressionValues).Returns(GetExpressionValues());
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Returns((string tag, string line) =>
{
@@ -105,6 +107,88 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Theory]
[InlineData("stop-commands", "1")]
[InlineData("", "1")]
[InlineData("set-env", "1")]
[InlineData("stop-commands", "true")]
[InlineData("", "true")]
[InlineData("set-env", "true")]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StopProcessCommand__AllowsInvalidStopTokens__IfEnvVarIsSet(string invalidToken, string allowUnsupportedStopCommandTokens)
{
using (TestHostContext hc = CreateTestContext())
{
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();
var expressionValues = new DictionaryContextData
{
["env"] =
#if OS_WINDOWS
new DictionaryContextData{ { Constants.Variables.Actions.AllowUnsupportedStopCommandTokens, new StringContextData(allowUnsupportedStopCommandTokens) }}
#else
new CaseSensitiveDictionaryContextData{ { Constants.Variables.Actions.AllowUnsupportedStopCommandTokens, new StringContextData(allowUnsupportedStopCommandTokens) }}
#endif
};
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
_ec.Setup(x => x.JobTelemetry).Returns(new List<JobTelemetry>());
Assert.True(_commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{invalidToken}", null));
}
}
[Theory]
[InlineData("stop-commands")]
[InlineData("")]
[InlineData("set-env")]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StopProcessCommand__FailOnInvalidStopTokens(string invalidToken)
{
using (TestHostContext hc = CreateTestContext())
{
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();
_ec.Setup(x => x.ExpressionValues).Returns(GetExpressionValues());
_ec.Setup(x => x.JobTelemetry).Returns(new List<JobTelemetry>());
Assert.Throws<Exception>(() => _commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{invalidToken}", null));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StopProcessCommandAcceptsValidToken()
{
var validToken = "randomToken";
using (TestHostContext hc = CreateTestContext())
{
_ec.Setup(x => x.ExpressionValues).Returns(GetExpressionValues());
Assert.True(_commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{validToken}", null));
Assert.False(_commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar", null));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, $"::{validToken}::", null));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar", null));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StopProcessCommandMasksValidTokenForEntireRun()
{
var validToken = "randomToken";
using (TestHostContext hc = CreateTestContext())
{
_ec.Setup(x => x.ExpressionValues).Returns(GetExpressionValues());
Assert.True(_commandManager.TryProcessCommand(_ec.Object, $"::stop-commands::{validToken}", null));
Assert.False(_commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar", null));
Assert.Equal("***", hc.SecretMasker.MaskSecrets(validToken));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, $"::{validToken}::", null));
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar", null));
Assert.Equal("***", hc.SecretMasker.MaskSecrets(validToken));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -202,15 +286,15 @@ namespace GitHub.Runner.Common.Tests.Worker
return 1;
});
var registeredCommands = new HashSet<string>(new string[1]{ "warning" });
var registeredCommands = new HashSet<string>(new string[1] { "warning" });
ActionCommand command;
// Columns when lines are different
ActionCommand.TryParseV2("::warning line=1,endLine=2,col=1,endColumn=2::this is a warning", registeredCommands, out command);
Assert.Equal("1", command.Properties["col"]);
IssueCommandExtension.ValidateLinesAndColumns(command, _ec.Object);
Assert.False(command.Properties.ContainsKey("col"));
// No lines with columns
ActionCommand.TryParseV2("::warning col=1,endColumn=2::this is a warning", registeredCommands, out command);
Assert.Equal("1", command.Properties["col"]);
@@ -375,5 +459,19 @@ namespace GitHub.Runner.Common.Tests.Worker
return hostContext;
}
private DictionaryContextData GetExpressionValues()
{
return new DictionaryContextData
{
["env"] =
#if OS_WINDOWS
new DictionaryContextData()
#else
new CaseSensitiveDictionaryContextData()
#endif
};
}
}
}

View File

@@ -1 +1 @@
2.283.0
2.283.4