Compare commits

...

15 Commits

Author SHA1 Message Date
Cory Miller
0be495a2f9 Detection for heredoc (#2738) 2023-08-08 11:12:42 -04:00
Ben Wells
9c8abf2a6e updating test cases and fixing normal style check 2023-07-26 15:53:57 +00:00
Ben Wells
45d079e5f3 syntax errors 2023-07-26 14:57:57 +00:00
Ben Wells
4da8c9e69a code review suggestion of checking heredocIndex < equalsIndex to short circuit the 'equals in delimiter' case 2023-07-26 14:51:57 +00:00
Ben Wells
3ee541e26a Update src/Test/L0/Worker/FileCommandTestBase.cs
Co-authored-by: Cory Miller <13227161+cory-miller@users.noreply.github.com>
2023-07-26 10:46:04 -04:00
Ben Wells
71600dec6e throw argument exception to guard against case where pos1 is greater than pos2 2023-07-26 14:44:37 +00:00
Ben Wells
62d0a70002 Update src/Test/L0/Worker/FileCommandTestBase.cs
Co-authored-by: Cory Miller <13227161+cory-miller@users.noreply.github.com>
2023-07-26 10:40:08 -04:00
Ben Wells
fc8a966a21 Update src/Runner.Worker/FileCommandManager.cs
Co-authored-by: Cory Miller <13227161+cory-miller@users.noreply.github.com>
2023-07-26 10:39:44 -04:00
Ben Wells
184098ac5d Update src/Runner.Worker/FileCommandManager.cs
Co-authored-by: Cory Miller <13227161+cory-miller@users.noreply.github.com>
2023-07-26 10:39:36 -04:00
Ben Wells
76e2904c63 Update FileCommandManager.cs 2023-07-21 16:05:30 -04:00
Ben Wells
2796fcdd87 Update FileCommandTestBase.cs to add additional heredoc edge cases 2023-07-21 15:55:35 -04:00
Ben Wells
c71eceaae3 Update heredoc detection to handle more edge cases 2023-07-21 15:49:51 -04:00
JoannaaKL
496904c0b7 Fix feature flag location (#2703) 2023-07-19 12:19:42 +02:00
Tingluo Huang
b91ad56f92 Check connectivity for endpoints requested by service. (#2691) 2023-07-17 08:36:20 -04:00
Ferenc Hammerl
f25c9dfba3 Fixes if:cancelled() composite steps not running and normal composite steps not interrupting when the job is cancelled. (#2638)
* Set composite step's action_status when job is cancelled

Also make composite step inherit timeout from parent

* Fix eof line
2023-07-10 13:32:30 +02:00
8 changed files with 205 additions and 33 deletions

View File

@@ -78,6 +78,7 @@ namespace GitHub.Runner.Worker
List<string> StepEnvironmentOverrides { get; }
ExecutionContext Root { get; }
ExecutionContext Parent { get; }
// Initialize
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
@@ -264,6 +265,14 @@ namespace GitHub.Runner.Worker
}
}
public ExecutionContext Parent
{
get
{
return _parentExecutionContext;
}
}
public JobContext JobContext
{
get
@@ -406,7 +415,7 @@ namespace GitHub.Runner.Worker
/// <summary>
/// An embedded execution context shares the same record ID, record name, logger,
/// and a linked cancellation token.
/// but NOT the cancellation token (just like workflow steps contexts - they don't share a token)
/// </summary>
public IExecutionContext CreateEmbeddedChild(
string scopeName,
@@ -416,7 +425,7 @@ namespace GitHub.Runner.Worker
Dictionary<string, string> intraActionState = null,
string siblingScopeName = null)
{
return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, cancellationTokenSource: CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token), intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName);
return Root.CreateChild(_record.Id, _record.Name, _record.Id.ToString("N"), scopeName, contextName, stage, logger: _logger, isEmbedded: true, cancellationTokenSource: null, intraActionState: intraActionState, embeddedId: embeddedId, siblingScopeName: siblingScopeName);
}
public void Start(string currentOperation = null)
@@ -597,9 +606,33 @@ namespace GitHub.Runner.Worker
if (timeout != null)
{
_cancellationTokenSource.CancelAfter(timeout.Value);
m_timeoutStartedAt = DateTime.UtcNow;
m_timeout = timeout.Value;
}
}
DateTime? m_timeoutStartedAt;
TimeSpan? m_timeout;
public TimeSpan? GetRemainingTimeout()
{
if (m_timeoutStartedAt != null && m_timeout != null)
{
var elapsedSinceTimeoutSet = DateTime.UtcNow - m_timeoutStartedAt.Value;
var remainingTimeout = m_timeout.Value - elapsedSinceTimeoutSet;
if (remainingTimeout.Ticks > 0)
{
return remainingTimeout;
}
else
{
// there was a timeout and it has expired
return TimeSpan.Zero;
}
}
// no timeout was ever set
return null;
}
public void Progress(int percentage, string currentOperation = null)
{
if (percentage > 100 || percentage < 0)
@@ -1435,7 +1468,7 @@ namespace GitHub.Runner.Worker
private bool logTemplateErrorsAsDebugMessages()
{
if (_executionContext.Global.EnvironmentVariables.TryGetValue(Constants.Runner.Features.LogTemplateErrorsAsDebugMessages, out var logErrorsAsDebug))
if (_executionContext.Global.Variables.TryGetValue(Constants.Runner.Features.LogTemplateErrorsAsDebugMessages, out var logErrorsAsDebug))
{
return StringUtil.ConvertToBoolean(logErrorsAsDebug, defaultValue: false);
}

View File

@@ -322,16 +322,28 @@ namespace GitHub.Runner.Worker
var equalsIndex = line.IndexOf("=", StringComparison.Ordinal);
var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal);
// Heredoc style NAME<<EOF (where EOF is typically randomly-generated Base64 and may include an '=' character)
// Heredoc style NAME=<<EOF (where EOF is typically randomly-generated Base64 and may include an '=' character)
// see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
if (heredocIndex >= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex))
// "key=<<EOF is also considered heredoc where the key contains an =.
// Also we detect heredoc if whitespace chars exist between = and <<. (ex: "key <<EOF")
var isHeredoc = heredocIndex >= 0 &&
(
equalsIndex < 0 ||
heredocIndex < equalsIndex ||
(
heredocIndex > equalsIndex &&
OnlyContainsWhiteSpaceBetweenPositions(line, equalsIndex, heredocIndex)
)
);
if (isHeredoc)
{
var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None);
if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1]))
{
throw new Exception($"Invalid format '{line}'. Name must not be empty and delimiter must not be empty");
}
key = split[0];
key = split[0].Trim().TrimEnd('=');
var delimiter = split[1];
var startIndex = index; // Start index of the value (inclusive)
var endIndex = index; // End index of the value (exclusive)
@@ -352,8 +364,8 @@ namespace GitHub.Runner.Worker
output = endIndex > startIndex ? text.Substring(startIndex, endIndex - startIndex) : string.Empty;
}
// Normal style NAME=VALUE
else if (equalsIndex >= 0 && heredocIndex < 0)
// Normal style NAME=VALUE, can have << in the value.
else if (equalsIndex >= 0)
{
var split = line.Split(new[] { '=' }, 2, StringSplitOptions.None);
if (string.IsNullOrEmpty(line))
@@ -383,6 +395,32 @@ namespace GitHub.Runner.Worker
return GetEnumerator();
}
// accepts a string, two indexes, and returns true if only whitespace chars exist between those two indexes (non-inclusive)
private static bool OnlyContainsWhiteSpaceBetweenPositions(string str, int pos1, int pos2)
{
// Check if the provided positions are valid
if (pos1 < 0 || pos2 < 0 || pos1 >= str.Length || pos2 >= str.Length)
{
throw new ArgumentOutOfRangeException("OnlyContainsWhiteSpaceBetweenPositions: Positions should be within the string length.");
}
// Ensure pos1 is always the smaller position
if (pos2 < pos1)
{
throw new ArgumentException("OnlyContainsWhiteSpaceBetweenPositions: pos1 must be less than or equal to pos2.");
}
for (int i = pos1 + 1; i < pos2; i++)
{
if (!char.IsWhiteSpace(str[i]))
{
return false;
}
}
return true;
}
private static string ReadLine(
string text,
ref int index)

View File

@@ -310,6 +310,7 @@ namespace GitHub.Runner.Worker.Handlers
// Mark job as cancelled
ExecutionContext.Root.Result = TaskResult.Canceled;
ExecutionContext.Root.JobContext.Status = ExecutionContext.Root.Result?.ToActionResult();
step.ExecutionContext.SetGitHubContext("action_status", (ExecutionContext.Root.Result?.ToActionResult() ?? ActionResult.Cancelled).ToString().ToLowerInvariant());
step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'.");
var conditionReTestTraceWriter = new ConditionTraceWriter(Trace, null); // host tracing only
@@ -420,6 +421,8 @@ namespace GitHub.Runner.Worker.Handlers
{
Trace.Info($"Starting: {step.DisplayName}");
step.ExecutionContext.Debug($"Starting: {step.DisplayName}");
// composite steps inherit the timeout from the parent, set by https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes
step.ExecutionContext.SetTimeout(step.ExecutionContext.Parent.GetRemainingTimeout());
await Common.Util.EncodingUtil.SetEncoding(HostContext, Trace, step.ExecutionContext.CancellationToken);

View File

@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -15,6 +16,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Services.Common;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Worker
@@ -34,12 +36,13 @@ namespace GitHub.Runner.Worker
public interface IJobExtension : IRunnerService
{
Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message);
void FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc);
Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc);
}
public sealed class JobExtension : RunnerService, IJobExtension
{
private readonly HashSet<string> _existingProcesses = new(StringComparer.OrdinalIgnoreCase);
private readonly List<Task<string>> _connectivityCheckTasks = new();
private bool _processCleanup;
private string _processLookupId = $"github_{Guid.NewGuid()}";
private CancellationTokenSource _diskSpaceCheckToken = new();
@@ -428,6 +431,22 @@ namespace GitHub.Runner.Worker
_diskSpaceCheckTask = CheckDiskSpaceAsync(context, _diskSpaceCheckToken.Token);
}
// Check server connectivity in background
ServiceEndpoint systemConnection = message.Resources.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
if (systemConnection.Data.TryGetValue("ConnectivityChecks", out var connectivityChecksPayload) &&
!string.IsNullOrEmpty(connectivityChecksPayload))
{
Trace.Info($"Start checking server connectivity.");
var checkUrls = StringUtil.ConvertFromJson<List<string>>(connectivityChecksPayload);
if (checkUrls?.Count > 0)
{
foreach (var checkUrl in checkUrls)
{
_connectivityCheckTasks.Add(CheckConnectivity(checkUrl));
}
}
}
return steps;
}
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
@@ -472,7 +491,7 @@ namespace GitHub.Runner.Worker
return reference;
}
public void FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc)
public async Task FinalizeJob(IExecutionContext jobContext, Pipelines.AgentJobRequestMessage message, DateTime jobStartTimeUtc)
{
Trace.Entering();
ArgUtil.NotNull(jobContext, nameof(jobContext));
@@ -649,6 +668,28 @@ namespace GitHub.Runner.Worker
{
_diskSpaceCheckToken.Cancel();
}
// Collect server connectivity check result
if (_connectivityCheckTasks.Count > 0)
{
try
{
Trace.Info($"Wait for all connectivity checks to finish.");
await Task.WhenAll(_connectivityCheckTasks);
foreach (var check in _connectivityCheckTasks)
{
var result = await check;
Trace.Info($"Connectivity check result: {result}");
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = result });
}
}
catch (Exception ex)
{
Trace.Error($"Fail to check server connectivity.");
Trace.Error(ex);
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"Fail to check server connectivity. {ex.Message}" });
}
}
}
catch (Exception ex)
{
@@ -664,6 +705,37 @@ namespace GitHub.Runner.Worker
}
}
private async Task<string> CheckConnectivity(string endpointUrl)
{
Trace.Info($"Check server connectivity for {endpointUrl}.");
string result = string.Empty;
using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
try
{
using (var httpClientHandler = HostContext.CreateHttpClientHandler())
using (var httpClient = new HttpClient(httpClientHandler))
{
httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents);
var response = await httpClient.GetAsync(endpointUrl, timeoutTokenSource.Token);
result = $"{endpointUrl}: {response.StatusCode}";
}
}
catch (Exception ex) when (ex is OperationCanceledException && timeoutTokenSource.IsCancellationRequested)
{
Trace.Error($"Request timeout during connectivity check: {ex}");
result = $"{endpointUrl}: timeout";
}
catch (Exception ex)
{
Trace.Error($"Catch exception during connectivity check: {ex}");
result = $"{endpointUrl}: {ex.Message}";
}
}
return result;
}
private async Task CheckDiskSpaceAsync(IExecutionContext context, CancellationToken token)
{
while (!token.IsCancellationRequested)

View File

@@ -229,7 +229,7 @@ namespace GitHub.Runner.Worker
finally
{
Trace.Info("Finalize job.");
jobExtension.FinalizeJob(jobContext, message, jobStartTimeUtc);
await jobExtension.FinalizeJob(jobContext, message, jobStartTimeUtc);
}
Trace.Info($"Job result after all job steps finish: {jobContext.Result ?? TaskResult.Succeeded}");

View File

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

View File

@@ -238,15 +238,26 @@ namespace GitHub.Runner.Common.Tests.Worker
"MY_KEY_4<<EOF",
"EOF EOF",
"EOF",
"MY_KEY_5=abc << def",
"MY_KEY_6= <<EOF",
"white space test",
"EOF",
"MY_KEY_7 <<=EOF=",
"abc",
"=EOF=",
string.Empty
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(4, _store.Count);
Assert.Equal(7, _store.Count);
Assert.Equal($"hello{BREAK}{BREAK}three{BREAK}", _store["MY_KEY_1"]);
Assert.Equal($"hello=two", _store["MY_KEY_2"]);
Assert.Equal($" EOF", _store["MY_KEY_3"]);
Assert.Equal($"EOF EOF", _store["MY_KEY_4"]);
Assert.Equal($"abc << def", _store["MY_KEY_5"]);
Assert.Equal($"white space test", _store["MY_KEY_6"]);
Assert.Equal($"abc", _store["MY_KEY_7"]);
}
}

View File

@@ -1,13 +1,13 @@
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using Moq;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Xunit;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using Moq;
using Xunit;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Common.Tests.Worker
@@ -105,6 +105,18 @@ namespace GitHub.Runner.Common.Tests.Worker
github["repository"] = new Pipelines.ContextData.StringContextData("actions/runner");
github["secret_source"] = new Pipelines.ContextData.StringContextData("Actions");
_message.ContextData.Add("github", github);
_message.Resources.Endpoints.Add(new ServiceEndpoint()
{
Name = WellKnownServiceEndpointNames.SystemVssConnection,
Url = new Uri("https://pipelines.actions.githubusercontent.com"),
Authorization = new EndpointAuthorization()
{
Scheme = "Test",
Parameters = {
{"AccessToken", "token"}
}
},
});
hc.SetSingleton(_actionManager.Object);
hc.SetSingleton(_config.Object);
@@ -231,7 +243,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void UploadDiganosticLogIfEnvironmentVariableSet()
public async Task UploadDiganosticLogIfEnvironmentVariableSet()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -244,7 +256,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
_diagnosticLogManager.Verify(x =>
x.UploadDiagnosticLogs(
@@ -259,7 +271,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void DontUploadDiagnosticLogIfEnvironmentVariableFalse()
public async Task DontUploadDiagnosticLogIfEnvironmentVariableFalse()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -272,7 +284,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
_diagnosticLogManager.Verify(x =>
x.UploadDiagnosticLogs(
@@ -287,14 +299,14 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void DontUploadDiagnosticLogIfEnvironmentVariableMissing()
public async Task DontUploadDiagnosticLogIfEnvironmentVariableMissing()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
_diagnosticLogManager.Verify(x =>
x.UploadDiagnosticLogs(
@@ -309,7 +321,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureFinalizeJobRunsIfMessageHasNoEnvironmentUrl()
public async Task EnsureFinalizeJobRunsIfMessageHasNoEnvironmentUrl()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -322,7 +334,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
}
@@ -331,7 +343,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureFinalizeJobHandlesNullEnvironmentUrl()
public async Task EnsureFinalizeJobHandlesNullEnvironmentUrl()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -347,7 +359,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
}
@@ -356,7 +368,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureFinalizeJobHandlesNullEnvironment()
public async Task EnsureFinalizeJobHandlesNullEnvironment()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -369,7 +381,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
}
@@ -397,7 +409,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var hookStart = result.First() as JobExtensionRunner;
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(Constants.Hooks.JobStartedStepName, hookStart.DisplayName);
Assert.Equal(Constants.Hooks.JobCompletedStepName, (_jobEc.PostJobSteps.Last() as JobExtensionRunner).DisplayName);
@@ -410,7 +422,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EnsureNoPreAndPostHookSteps()
public async Task EnsureNoPreAndPostHookSteps()
{
using (TestHostContext hc = CreateTestContext())
{
@@ -425,7 +437,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var x = _jobEc.JobSteps;
jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
Assert.Equal(TaskResult.Succeeded, _jobEc.Result);
Assert.Equal(0, _jobEc.PostJobSteps.Count);