mirror of
https://github.com/actions/runner.git
synced 2026-07-04 19:45:31 +08:00
Compare commits
3 Commits
dependabot
...
rentziass/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06ae0d86a3 | ||
|
|
e615bd8a80 | ||
|
|
d8a18c194c |
@@ -5,8 +5,8 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG RUNNER_VERSION
|
||||
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
|
||||
ARG DOCKER_VERSION=29.5.3
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG DOCKER_VERSION=29.5.0
|
||||
ARG BUILDX_VERSION=0.34.0
|
||||
|
||||
RUN apt update -y && apt install curl unzip -y
|
||||
|
||||
|
||||
@@ -1,40 +1,36 @@
|
||||
## What's Changed
|
||||
* Bump System.ServiceProcess.ServiceController from 10.0.6 to 10.0.7 by @dependabot[bot] in https://github.com/actions/runner/pull/4370
|
||||
* Bump @actions/glob from 0.6.1 to 0.7.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4367
|
||||
* feat: propagate actions dependencies by @nodeselector in https://github.com/actions/runner/pull/4372
|
||||
* Not retry and report action download 403. by @TingluoHuang in https://github.com/actions/runner/pull/4391
|
||||
* Update setup job starting logs by @GitPaulo in https://github.com/actions/runner/pull/4383
|
||||
* fix: expand commit hash regex to support SHA-256 (64-char) hashes by @yaananth in https://github.com/actions/runner/pull/4347
|
||||
* Move dap setup to setup job step by @rentziass in https://github.com/actions/runner/pull/4403
|
||||
* Add support for Ubuntu 26.04 (liblttng-ust1t64, libicu77-80) by @dvaldivia in https://github.com/actions/runner/pull/4394
|
||||
* Update dotnet sdk to latest version @8.0.421 by @github-actions[bot] in https://github.com/actions/runner/pull/4428
|
||||
* Update Docker to v29.5.0 and Buildx to v0.34.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4425
|
||||
* Execute debugger REPL commands inside job container by @rentziass in https://github.com/actions/runner/pull/4420
|
||||
* Send welcome message in debugger console on connect by @rentziass in https://github.com/actions/runner/pull/4419
|
||||
* Update snapshot-if context and functions by @drielenr in https://github.com/actions/runner/pull/4443
|
||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4452
|
||||
* Allow disable node v8 maglev jit compiler on node24. by @TingluoHuang in https://github.com/actions/runner/pull/4447
|
||||
* Update Node 24 default date to June 16th, 2026 by @salmanmkc in https://github.com/actions/runner/pull/4462
|
||||
* Populate telemetry for non-action post-job steps by @drielenr in https://github.com/actions/runner/pull/4463
|
||||
* Add SDK types and results plumbing for background step control by @lokesh755 in https://github.com/actions/runner/pull/4472
|
||||
* Add job execution view model by @rentziass in https://github.com/actions/runner/pull/4470
|
||||
* Add thread-safety locks to StepsContext by @lokesh755 in https://github.com/actions/runner/pull/4475
|
||||
* Add background step deferral infrastructure and metadata plumbing by @lokesh755 in https://github.com/actions/runner/pull/4479
|
||||
* Wire job execution view into DAP by @rentziass in https://github.com/actions/runner/pull/4471
|
||||
* Background steps execution engine by @lokesh755 in https://github.com/actions/runner/pull/4476
|
||||
* Update Docker to v29.5.2 and Buildx to v0.34.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4451
|
||||
* BrokerServer should not retry on 401. by @TingluoHuang in https://github.com/actions/runner/pull/4445
|
||||
* Add new env var to allow single-prefix multiline logs on stdout by @nuclearpidgeon in https://github.com/actions/runner/pull/4424
|
||||
* Bump Microsoft.DevTunnels.Connections from 1.3.39 to 1.3.48 by @dependabot[bot] in https://github.com/actions/runner/pull/4441
|
||||
* Bump System.Formats.Asn1 and System.Security.Cryptography.Pkcs by @dependabot[bot] in https://github.com/actions/runner/pull/4369
|
||||
* Bump flatted from 3.2.7 to 3.4.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4307
|
||||
* Add DAP server by @rentziass in https://github.com/actions/runner/pull/4298
|
||||
* Bump @typescript-eslint/eslint-plugin from 8.57.1 to 8.57.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4310
|
||||
* Remove AllowCaseFunction feature flag by @ericsciple in https://github.com/actions/runner/pull/4316
|
||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4319
|
||||
* Batch and deduplicate action resolution across composite depths by @stefanpenner in https://github.com/actions/runner/pull/4296
|
||||
* Add support for Bearer token in action archive downloads by @TingluoHuang in https://github.com/actions/runner/pull/4321
|
||||
* Bump brace-expansion in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4318
|
||||
* Add devtunnel connection for debugger jobs by @rentziass in https://github.com/actions/runner/pull/4317
|
||||
* Update Docker to v29.3.1 and Buildx to v0.33.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4324
|
||||
* Bump @typescript-eslint/eslint-plugin from 8.57.2 to 8.58.1 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4327
|
||||
* Bump actions/github-script from 8 to 9 by @dependabot[bot] in https://github.com/actions/runner/pull/4331
|
||||
* Bump typescript from 5.9.3 to 6.0.2 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4329
|
||||
* fix: only show changed versions in node upgrade PR description by @salmanmkc in https://github.com/actions/runner/pull/4332
|
||||
* Bump System.Formats.Asn1, Cryptography.Pkcs, ProtectedData, ServiceController, CodePages, Threading.Channels, @actions/glob, @typescript-eslint/parser, lint-staged, picomatch by @Copilot in https://github.com/actions/runner/pull/4333
|
||||
* feat: add `job.workflow_*` typed accessors to JobContext by @salmanmkc in https://github.com/actions/runner/pull/4335
|
||||
* Add WS bridge over DAP TCP server by @rentziass in https://github.com/actions/runner/pull/4328
|
||||
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4355
|
||||
* Bump Docker version to 29.4.0 by @Copilot in https://github.com/actions/runner/pull/4352
|
||||
* Update dotnet sdk to latest version @8.0.420 by @github-actions[bot] in https://github.com/actions/runner/pull/4356
|
||||
* Bump @typescript-eslint/parser from 8.58.1 to 8.59.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4360
|
||||
* Bump System.Formats.Asn1 and System.Security.Cryptography.Pkcs by @dependabot[bot] in https://github.com/actions/runner/pull/4362
|
||||
* Add vulnerability-alerts permission by @salmanmkc in https://github.com/actions/runner/pull/4350
|
||||
* Bump @typescript-eslint/eslint-plugin from 8.58.1 to 8.59.0 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4359
|
||||
* Bump System.ServiceProcess.ServiceController from 10.0.3 to 10.0.6 by @dependabot[bot] in https://github.com/actions/runner/pull/4358
|
||||
* Bump typescript from 6.0.2 to 6.0.3 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4353
|
||||
* Bump Microsoft.DevTunnels.Connections from 1.3.16 to 1.3.39 by @dependabot[bot] in https://github.com/actions/runner/pull/4339
|
||||
|
||||
## New Contributors
|
||||
* @GitPaulo made their first contribution in https://github.com/actions/runner/pull/4383
|
||||
* @dvaldivia made their first contribution in https://github.com/actions/runner/pull/4394
|
||||
* @drielenr made their first contribution in https://github.com/actions/runner/pull/4443
|
||||
* @nuclearpidgeon made their first contribution in https://github.com/actions/runner/pull/4424
|
||||
* @stefanpenner made their first contribution in https://github.com/actions/runner/pull/4296
|
||||
|
||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.334.0...v2.335.0
|
||||
**Full Changelog**: https://github.com/actions/runner/compare/v2.333.1...v2.334.0
|
||||
|
||||
_Note: Actions Runner follows a progressive release policy, so the latest release might not be available to your enterprise, organization, or repository yet.
|
||||
To confirm which version of the Actions Runner you should expect, please view the download instructions for your enterprise, organization, or repository.
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace GitHub.Runner.Common
|
||||
|
||||
public bool ShouldRetryException(Exception ex)
|
||||
{
|
||||
if (ex is AccessDeniedException || ex is VssUnauthorizedException || ex is RunnerNotFoundException || ex is HostedRunnerDeprovisionedException)
|
||||
if (ex is AccessDeniedException || ex is RunnerNotFoundException || ex is HostedRunnerDeprovisionedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -308,7 +308,6 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string ForcedInternalNodeVersion = "ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION";
|
||||
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
|
||||
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
|
||||
public static readonly string DisableStdoutMultilineLogPrefixing = "ACTIONS_RUNNER_DISABLE_STDOUT_MULTILINE_LOG_PREFIXING";
|
||||
public static readonly string ActionArchiveCacheDirectory = "ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE";
|
||||
public static readonly string SymlinkCachedActions = "ACTIONS_RUNNER_SYMLINK_CACHED_ACTIONS";
|
||||
public static readonly string EmitCompositeMarkers = "ACTIONS_RUNNER_EMIT_COMPOSITE_MARKERS";
|
||||
|
||||
@@ -837,15 +837,6 @@ namespace GitHub.Runner.Common
|
||||
timelineRecord.Variables[variable.Key] = variable.Value.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Merge background step metadata
|
||||
if (rec.IsBackground)
|
||||
{
|
||||
timelineRecord.IsBackground = rec.IsBackground;
|
||||
}
|
||||
timelineRecord.BackgroundControlType = rec.BackgroundControlType ?? timelineRecord.BackgroundControlType;
|
||||
timelineRecord.BackgroundControlStepIds = rec.BackgroundControlStepIds ?? timelineRecord.BackgroundControlStepIds;
|
||||
timelineRecord.ParallelGroupId = rec.ParallelGroupId ?? timelineRecord.ParallelGroupId;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -9,12 +9,10 @@ namespace GitHub.Runner.Common
|
||||
public sealed class StdoutTraceListener : ConsoleTraceListener
|
||||
{
|
||||
private readonly string _hostType;
|
||||
private readonly bool _disablePrefixMultilineLogs = false;
|
||||
|
||||
public StdoutTraceListener(string hostType)
|
||||
{
|
||||
this._hostType = hostType;
|
||||
this._disablePrefixMultilineLogs = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Agent.DisableStdoutMultilineLogPrefixing));
|
||||
}
|
||||
|
||||
// Copied and modified slightly from .Net Core source code. Modification was required to make it compile.
|
||||
@@ -28,20 +26,11 @@ namespace GitHub.Runner.Common
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
if (!this._disablePrefixMultilineLogs)
|
||||
{
|
||||
var messageLines = message.Split(Environment.NewLine);
|
||||
foreach (var messageLine in messageLines)
|
||||
{
|
||||
WriteHeader(source, eventType, id);
|
||||
WriteLine(messageLine);
|
||||
WriteFooter(eventCache);
|
||||
}
|
||||
}
|
||||
else
|
||||
var messageLines = message.Split(Environment.NewLine);
|
||||
foreach (var messageLine in messageLines)
|
||||
{
|
||||
WriteHeader(source, eventType, id);
|
||||
WriteLine(message);
|
||||
WriteLine(messageLine);
|
||||
WriteFooter(eventCache);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.9" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.3" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -282,15 +282,8 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
if (context.DeferredEnvironmentVariables != null)
|
||||
{
|
||||
context.DeferredEnvironmentVariables[envName] = command.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Global.EnvironmentVariables[envName] = command.Data;
|
||||
context.SetEnvContext(envName, command.Data);
|
||||
}
|
||||
context.Global.EnvironmentVariables[envName] = command.Data;
|
||||
context.SetEnvContext(envName, command.Data);
|
||||
context.Debug($"{envName}='{command.Data}'");
|
||||
}
|
||||
|
||||
@@ -341,15 +334,8 @@ namespace GitHub.Runner.Worker
|
||||
throw new Exception("Required field 'name' is missing in ##[set-output] command.");
|
||||
}
|
||||
|
||||
if (context.DeferredOutputs != null)
|
||||
{
|
||||
context.DeferredOutputs[outputName] = command.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.SetOutput(outputName, command.Data, out var reference);
|
||||
context.Debug($"{reference}='{command.Data}'");
|
||||
}
|
||||
context.SetOutput(outputName, command.Data, out var reference);
|
||||
context.Debug($"{reference}='{command.Data}'");
|
||||
}
|
||||
|
||||
private static class SetOutputCommandProperties
|
||||
@@ -479,16 +465,8 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
ArgUtil.NotNullOrEmpty(command.Data, "path");
|
||||
if (context.DeferredPrependPath != null)
|
||||
{
|
||||
context.DeferredPrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
||||
context.DeferredPrependPath.Add(command.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Global.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
||||
context.Global.PrependPath.Add(command.Data);
|
||||
}
|
||||
context.Global.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
||||
context.Global.PrependPath.Add(command.Data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure data for control-flow steps (wait, wait-all, cancel).
|
||||
/// Type uses Pipelines.BackgroundControlTypes string constants.
|
||||
/// </summary>
|
||||
public sealed class BackgroundStepControlFlowData
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public Guid StepId { get; set; }
|
||||
public string StepName { get; set; }
|
||||
|
||||
// Target step IDs (for wait: steps to wait for; for cancel: steps to cancel)
|
||||
public string[] StepIds { get; set; }
|
||||
|
||||
// Parallel group ID for grouping steps in the UI
|
||||
public string ParallelGroupId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,394 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
[ServiceLocator(Default = typeof(BackgroundStepCoordinator))]
|
||||
public interface IBackgroundStepCoordinator : IRunnerService
|
||||
{
|
||||
void InitializeCoordinator(int maxConcurrent);
|
||||
void StartBackgroundStep(IStep step, CancellationToken jobCancellationToken);
|
||||
Task<TaskResult> WaitForUnwaitedStepsAsync(CancellationToken cancellationToken);
|
||||
Task RunControlFlowAsync(IExecutionContext stepContext, object data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coordinates background step execution, waiting, cancellation, and deferred state.
|
||||
/// Extracted from StepsRunner so the main step loop stays clean.
|
||||
/// </summary>
|
||||
public sealed class BackgroundStepCoordinator : RunnerService, IBackgroundStepCoordinator
|
||||
{
|
||||
private const int DefaultMaxBackgroundSteps = 10;
|
||||
private readonly Dictionary<string, (IStep Step, Task Task, CancellationTokenSource Cts)> _backgroundSteps = new();
|
||||
|
||||
// IDs of background steps that have already been completed (waited on or canceled).
|
||||
// Used to avoid waiting on or flushing the same step more than once.
|
||||
private readonly HashSet<string> _completedStepIds = new();
|
||||
|
||||
// IDs of background steps that were explicitly canceled via a `cancel` control step.
|
||||
// These steps are expected to be canceled, so their (Canceled) result must not be
|
||||
// merged into the overall job result.
|
||||
private readonly HashSet<string> _explicitlyCanceledStepIds = new();
|
||||
private SemaphoreSlim _backgroundSlotSemaphore = new SemaphoreSlim(DefaultMaxBackgroundSteps);
|
||||
|
||||
/// <summary>
|
||||
/// Reset per-job state. Call at the start of each job.
|
||||
/// </summary>
|
||||
public void InitializeCoordinator(int maxConcurrent)
|
||||
{
|
||||
_backgroundSteps.Clear();
|
||||
_completedStepIds.Clear();
|
||||
_explicitlyCanceledStepIds.Clear();
|
||||
var max = maxConcurrent > 0 ? maxConcurrent : DefaultMaxBackgroundSteps;
|
||||
_backgroundSlotSemaphore = new SemaphoreSlim(max);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Starting background steps
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Prepare and launch a background step. Does not block the caller.
|
||||
/// </summary>
|
||||
public void StartBackgroundStep(IStep step, CancellationToken jobCancellationToken)
|
||||
{
|
||||
var stepId = step.ExecutionContext?.ContextName ?? step.DisplayName;
|
||||
|
||||
// Isolate GitHubContext so concurrent steps don't overwrite each other's GITHUB_OUTPUT paths
|
||||
if (step.ExecutionContext.ExpressionValues.TryGetValue("github", out var ghCtx) && ghCtx is GitHubContext sharedGitHub)
|
||||
{
|
||||
step.ExecutionContext.ExpressionValues["github"] = sharedGitHub.ShallowCopy();
|
||||
}
|
||||
|
||||
var bgCts = CancellationTokenSource.CreateLinkedTokenSource(jobCancellationToken);
|
||||
|
||||
// Evaluate timeout on the main thread (needs expression context)
|
||||
var timeoutMinutes = 0;
|
||||
try
|
||||
{
|
||||
var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator();
|
||||
timeoutMinutes = templateEvaluator.EvaluateStepTimeout(step.Timeout, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Info($"Error determining timeout for background step '{stepId}': {ex.Message}");
|
||||
}
|
||||
|
||||
var task = ExecuteBackgroundStepCoreAsync(step, bgCts, stepId, timeoutMinutes);
|
||||
_backgroundSteps[stepId] = (step, task, bgCts);
|
||||
Trace.Info($"Background step '{stepId}' queued (slot will be acquired asynchronously).");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Safety net
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// Drain any background steps that weren't already waited on by an explicit wait/cancel
|
||||
// control step, then merge the final results of all background steps into a single result
|
||||
// for the caller to fold into the job result.
|
||||
public async Task<TaskResult> WaitForUnwaitedStepsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var unwaitedIds = _backgroundSteps.Keys.Where(id => !_completedStepIds.Contains(id)).ToList();
|
||||
if (unwaitedIds.Count > 0)
|
||||
{
|
||||
Trace.Info($"Safety net: {unwaitedIds.Count} unwaited background step(s) at post-job boundary: {string.Join(", ", unwaitedIds)}");
|
||||
await WaitForStepTasksAsync(unwaitedIds, cancellationToken);
|
||||
CompleteWaitedSteps(unwaitedIds);
|
||||
}
|
||||
|
||||
var result = TaskResult.Succeeded;
|
||||
foreach (var (stepId, (step, _, _)) in _backgroundSteps)
|
||||
{
|
||||
// A step that succeeded does not set a Result by default, so a missing
|
||||
// value means the step succeeded and there is nothing to merge.
|
||||
if (!step.ExecutionContext.Result.HasValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// A step explicitly canceled via a `cancel` control step is expected to be canceled,
|
||||
// so a Canceled result must not influence the overall job result. However, if the step
|
||||
// failed (e.g. before the cancellation took effect), that failure should still count.
|
||||
if (_explicitlyCanceledStepIds.Contains(stepId) &&
|
||||
step.ExecutionContext.Result.Value == TaskResult.Canceled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result = TaskResultUtil.MergeTaskResults(result, step.ExecutionContext.Result.Value);
|
||||
}
|
||||
|
||||
if (result != TaskResult.Succeeded)
|
||||
{
|
||||
Trace.Info($"Background steps reported result '{result}' to caller.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Control-flow step dispatch
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Execute a control-flow step (wait, wait-all, cancel) and propagate results.
|
||||
/// </summary>
|
||||
public async Task RunControlFlowAsync(IExecutionContext stepContext, object data)
|
||||
{
|
||||
var controlFlow = data as BackgroundStepControlFlowData;
|
||||
switch (controlFlow.Type)
|
||||
{
|
||||
case Pipelines.BackgroundControlTypes.Wait:
|
||||
{
|
||||
var ids = controlFlow.StepIds ?? Array.Empty<string>();
|
||||
stepContext.Output($"Waiting for background step(s) to complete: {DescribeSteps(ids)}");
|
||||
await WaitForStepTasksAsync(ids, stepContext.CancellationToken);
|
||||
stepContext.Result = CompleteWaitedSteps(ids);
|
||||
ReportCompletedSteps(stepContext, "Finished waiting for background step(s).", ids);
|
||||
break;
|
||||
}
|
||||
|
||||
case Pipelines.BackgroundControlTypes.WaitAll:
|
||||
{
|
||||
var remaining = _backgroundSteps.Keys.Where(id => !_completedStepIds.Contains(id)).ToList();
|
||||
stepContext.Output(remaining.Count > 0
|
||||
? $"Waiting for all background step(s) to complete: {DescribeSteps(remaining)}"
|
||||
: "No background steps remaining to wait for.");
|
||||
await WaitForStepTasksAsync(remaining, stepContext.CancellationToken);
|
||||
stepContext.Result = CompleteWaitedSteps(remaining);
|
||||
ReportCompletedSteps(stepContext, "Finished waiting for all background step(s).", remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
case Pipelines.BackgroundControlTypes.Cancel:
|
||||
{
|
||||
var cancelIds = controlFlow.StepIds ?? Array.Empty<string>();
|
||||
stepContext.Output($"Cancelling background step(s): {DescribeSteps(cancelIds)}");
|
||||
await CancelStepsAsync(controlFlow.StepIds);
|
||||
stepContext.Result = TaskResult.Succeeded;
|
||||
ReportCompletedSteps(stepContext, "Finished cancelling background step(s).", cancelIds);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unknown background step control type '{controlFlow.Type}'.");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// Resolve background step IDs to their display names for customer-facing output.
|
||||
private string DescribeSteps(IEnumerable<string> stepIds)
|
||||
{
|
||||
var names = stepIds
|
||||
.Select(id => _backgroundSteps.TryGetValue(id, out var entry) ? entry.Step.DisplayName : id)
|
||||
.ToList();
|
||||
return names.Count > 0 ? string.Join(", ", names) : "(none)";
|
||||
}
|
||||
|
||||
// Emit a completion summary plus the final result of each affected background step.
|
||||
private void ReportCompletedSteps(IExecutionContext stepContext, string summary, IEnumerable<string> stepIds)
|
||||
{
|
||||
stepContext.Output(summary);
|
||||
foreach (var id in stepIds)
|
||||
{
|
||||
if (_backgroundSteps.TryGetValue(id, out var entry))
|
||||
{
|
||||
var result = entry.Step.ExecutionContext.Result?.ToString() ?? "Unknown";
|
||||
stepContext.Output($" {entry.Step.DisplayName}: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteBackgroundStepCoreAsync(
|
||||
IStep step, CancellationTokenSource bgCts,
|
||||
string stepId, int timeoutMinutes)
|
||||
{
|
||||
Trace.Info($"Background step '{stepId}' waiting for slot.");
|
||||
await _backgroundSlotSemaphore.WaitAsync(bgCts.Token);
|
||||
Trace.Info($"Background step '{stepId}' acquired slot.");
|
||||
|
||||
step.ExecutionContext.Start();
|
||||
|
||||
if (timeoutMinutes > 0)
|
||||
{
|
||||
step.ExecutionContext.SetTimeout(TimeSpan.FromMinutes(timeoutMinutes));
|
||||
}
|
||||
|
||||
using var cancelReg = bgCts.Token.Register(() =>
|
||||
{
|
||||
Trace.Info($"Background step '{stepId}': cancellation signalled, sending CancelToken to process.");
|
||||
step.ExecutionContext.CancelToken();
|
||||
});
|
||||
|
||||
TaskResult? result = null;
|
||||
try
|
||||
{
|
||||
await step.RunAsync();
|
||||
result = step.ExecutionContext.Result ?? TaskResult.Succeeded;
|
||||
}
|
||||
catch (OperationCanceledException) when (bgCts.Token.IsCancellationRequested)
|
||||
{
|
||||
result = TaskResult.Canceled;
|
||||
}
|
||||
catch (OperationCanceledException) when (step.ExecutionContext.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Trace.Info($"Background step '{stepId}' timed out after {timeoutMinutes} minutes.");
|
||||
step.ExecutionContext.Error($"The background step '{step.DisplayName}' has timed out after {timeoutMinutes} minutes.");
|
||||
result = TaskResult.Failed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Info($"Background step '{stepId}' failed: {ex.Message}");
|
||||
step.ExecutionContext.Error(ex);
|
||||
result = TaskResult.Failed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_backgroundSlotSemaphore.Release();
|
||||
|
||||
if (step.ExecutionContext.CommandResult != null)
|
||||
{
|
||||
result = TaskResultUtil.MergeTaskResults(result, step.ExecutionContext.CommandResult.Value);
|
||||
}
|
||||
|
||||
step.ExecutionContext.Result = result;
|
||||
step.ExecutionContext.ApplyContinueOnError(step.ContinueOnError);
|
||||
|
||||
step.ExecutionContext.Complete(step.ExecutionContext.Result);
|
||||
Trace.Info($"Background step '{stepId}' completed with result: {step.ExecutionContext.Result}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelStepsAsync(string[] cancelStepIds)
|
||||
{
|
||||
if (cancelStepIds == null || cancelStepIds.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark these steps as expected-to-be-canceled so their result does not
|
||||
// affect the overall job result.
|
||||
foreach (var id in cancelStepIds)
|
||||
{
|
||||
_explicitlyCanceledStepIds.Add(id);
|
||||
}
|
||||
|
||||
var idsToCancel = cancelStepIds
|
||||
.Where(id => _backgroundSteps.ContainsKey(id) && !_backgroundSteps[id].Task.IsCompleted)
|
||||
.ToArray();
|
||||
|
||||
if (idsToCancel.Length > 0)
|
||||
{
|
||||
Trace.Info($"Cancelling {idsToCancel.Length} background step(s): {string.Join(", ", idsToCancel)}");
|
||||
await CancelWithGracePeriodAsync(idsToCancel);
|
||||
}
|
||||
|
||||
// Flush deferred state and mark canceled steps as completed.
|
||||
CompleteWaitedSteps(cancelStepIds);
|
||||
}
|
||||
|
||||
private async Task WaitForStepTasksAsync(IEnumerable<string> stepIds, CancellationToken cancellationToken)
|
||||
{
|
||||
var ids = stepIds.ToList();
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var stepId in ids)
|
||||
{
|
||||
if (_backgroundSteps.TryGetValue(stepId, out var entry) && !entry.Task.IsCompleted)
|
||||
{
|
||||
tasks.Add(entry.Task);
|
||||
}
|
||||
else if (!_backgroundSteps.ContainsKey(stepId))
|
||||
{
|
||||
Trace.Info($"Wait references unknown background step: {stepId}");
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
Trace.Info($"Waiting for {tasks.Count} background step(s)...");
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(tasks).WaitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Trace.Info("Wait interrupted by job cancellation — cancelling background steps.");
|
||||
await CancelWithGracePeriodAsync(ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelWithGracePeriodAsync(IEnumerable<string> stepIds, double graceSeconds = 7.5)
|
||||
{
|
||||
var cancelledSteps = new List<(string StepId, Task Task, IStep Step)>();
|
||||
foreach (var stepId in stepIds)
|
||||
{
|
||||
if (_backgroundSteps.TryGetValue(stepId, out var entry) && !entry.Task.IsCompleted)
|
||||
{
|
||||
entry.Step.ExecutionContext.CancelToken();
|
||||
entry.Cts.Cancel();
|
||||
cancelledSteps.Add((stepId, entry.Task, entry.Step));
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelledSteps.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(cancelledSteps.Select(s => s.Task)).WaitAsync(TimeSpan.FromSeconds(graceSeconds));
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Trace.Info($"Some background steps did not terminate within {graceSeconds}s grace period.");
|
||||
|
||||
// The step tasks above never completed, so their finally block never ran and
|
||||
// their result was never set. Force-mark them as canceled so the abandoned
|
||||
// steps still report a terminal result.
|
||||
foreach (var (stepId, task, step) in cancelledSteps)
|
||||
{
|
||||
if (!task.IsCompleted && !step.ExecutionContext.Result.HasValue)
|
||||
{
|
||||
step.ExecutionContext.Result = TaskResult.Canceled;
|
||||
Trace.Info($"Background step '{stepId}' did not terminate within grace period; marking as canceled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TaskResult CompleteWaitedSteps(IEnumerable<string> stepIds)
|
||||
{
|
||||
var result = TaskResult.Succeeded;
|
||||
foreach (var id in stepIds)
|
||||
{
|
||||
_completedStepIds.Add(id);
|
||||
if (_backgroundSteps.TryGetValue(id, out var entry))
|
||||
{
|
||||
// Flush deferred state for the completed step.
|
||||
entry.Step.ExecutionContext.FlushDeferredOutputs();
|
||||
entry.Step.ExecutionContext.FlushDeferredEnvironment();
|
||||
entry.Step.ExecutionContext.FlushDeferredOutcomeConclusion();
|
||||
Trace.Info($"Flushed deferred state for background step '{id}'.");
|
||||
|
||||
if (entry.Step.ExecutionContext.Result.HasValue)
|
||||
{
|
||||
result = TaskResultUtil.MergeTaskResults(result, entry.Step.ExecutionContext.Result.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
JobId = string.IsNullOrWhiteSpace(jobId) ? "job" : jobId;
|
||||
|
||||
_preEntries.Add(new SourceEntry("Set up job"));
|
||||
_preEntries.Add(new SourceEntry("Setup job"));
|
||||
AddSteps(steps);
|
||||
AddPredictedPostSteps(predictedPostSteps);
|
||||
AddSteps(initialPostSteps);
|
||||
|
||||
@@ -77,23 +77,14 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
List<string> StepEnvironmentOverrides { get; }
|
||||
|
||||
bool IsBackground { get; }
|
||||
|
||||
IExecutionContext Root { get; }
|
||||
|
||||
// Initialize
|
||||
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
|
||||
void CancelToken();
|
||||
IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, List<Issue> embeddedIssueCollector = null, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null, TimeSpan? timeout = null, bool isBackground = false, string backgroundControlType = null, string[] backgroundControlStepIds = null, string parallelGroupId = null);
|
||||
IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, ActionRunStage stage, Dictionary<string, string> intraActionState = null, int? recordOrder = null, IPagingLogger logger = null, bool isEmbedded = false, List<Issue> embeddedIssueCollector = null, CancellationTokenSource cancellationTokenSource = null, Guid embeddedId = default(Guid), string siblingScopeName = null, TimeSpan? timeout = null);
|
||||
IExecutionContext CreateEmbeddedChild(string scopeName, string contextName, Guid embeddedId, ActionRunStage stage, Dictionary<string, string> intraActionState = null, string siblingScopeName = null);
|
||||
|
||||
|
||||
// Background step deferral properties
|
||||
Dictionary<string, string> DeferredOutputs { get; set; }
|
||||
Dictionary<string, string> DeferredEnvironmentVariables { get; set; }
|
||||
List<string> DeferredPrependPath { get; set; }
|
||||
bool DeferOutcomeConclusion { get; set; }
|
||||
|
||||
// logging
|
||||
long Write(string tag, string message);
|
||||
void QueueAttachFile(string type, string name, string filePath);
|
||||
@@ -109,12 +100,6 @@ namespace GitHub.Runner.Worker
|
||||
void SetGitHubContext(string name, string value);
|
||||
void SetOutput(string name, string value, out string reference);
|
||||
void SetTimeout(TimeSpan? timeout);
|
||||
|
||||
// Background step deferral flush methods
|
||||
void FlushDeferredOutputs();
|
||||
void FlushDeferredEnvironment();
|
||||
void FlushDeferredOutcomeConclusion();
|
||||
|
||||
void AddIssue(Issue issue, ExecutionContextLogOptions logOptions);
|
||||
void Progress(int percentage, string currentOperation = null);
|
||||
void UpdateDetailTimelineRecord(TimelineRecord record);
|
||||
@@ -231,9 +216,6 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public bool EchoOnActionCommand { get; set; }
|
||||
|
||||
// Whether this step runs in the background
|
||||
public bool IsBackground => _record.IsBackground;
|
||||
|
||||
// An embedded execution context shares the same record ID, record name, and logger
|
||||
// as its enclosing execution context.
|
||||
public bool IsEmbedded { get; private init; }
|
||||
@@ -297,12 +279,6 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public List<string> StepEnvironmentOverrides { get; } = new List<string>();
|
||||
|
||||
// Background step deferral properties
|
||||
public Dictionary<string, string> DeferredOutputs { get; set; }
|
||||
public Dictionary<string, string> DeferredEnvironmentVariables { get; set; }
|
||||
public List<string> DeferredPrependPath { get; set; }
|
||||
public bool DeferOutcomeConclusion { get; set; }
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
@@ -397,11 +373,7 @@ namespace GitHub.Runner.Worker
|
||||
CancellationTokenSource cancellationTokenSource = null,
|
||||
Guid embeddedId = default(Guid),
|
||||
string siblingScopeName = null,
|
||||
TimeSpan? timeout = null,
|
||||
bool isBackground = false,
|
||||
string backgroundControlType = null,
|
||||
string[] backgroundControlStepIds = null,
|
||||
string parallelGroupId = null)
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
Trace.Entering();
|
||||
|
||||
@@ -442,24 +414,6 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
child.EchoOnActionCommand = EchoOnActionCommand;
|
||||
|
||||
// Set background step metadata before InitializeTimelineRecord so it's included in the first update
|
||||
if (isBackground || backgroundControlType != null || parallelGroupId != null)
|
||||
{
|
||||
child._record.IsBackground = isBackground;
|
||||
child._record.BackgroundControlType = backgroundControlType;
|
||||
child._record.BackgroundControlStepIds = backgroundControlStepIds;
|
||||
child._record.ParallelGroupId = parallelGroupId;
|
||||
|
||||
// Initialize deferred state for background steps — flushed at wait/wait-all
|
||||
if (isBackground)
|
||||
{
|
||||
child.DeferredOutputs = new Dictionary<string, string>();
|
||||
child.DeferredEnvironmentVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
child.DeferredPrependPath = new List<string>();
|
||||
child.DeferOutcomeConclusion = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (recordOrder != null)
|
||||
{
|
||||
child.InitializeTimelineRecord(_mainTimelineId, recordId, _record.Id, ExecutionContextType.Task, displayName, refName, recordOrder, embedded: isEmbedded);
|
||||
@@ -572,11 +526,7 @@ namespace GitHub.Runner.Worker
|
||||
Type = StepTelemetry?.Type,
|
||||
StartedAt = _record.StartTime,
|
||||
CompletedAt = _record.FinishTime,
|
||||
Annotations = new List<Annotation>(),
|
||||
// Populate background step metadata from timeline record fields
|
||||
IsBackground = _record.IsBackground,
|
||||
BackgroundControlType = _record.BackgroundControlType,
|
||||
BackgroundControlStepIds = _record.BackgroundControlStepIds
|
||||
Annotations = new List<Annotation>()
|
||||
};
|
||||
|
||||
_record.Issues?.ForEach(issue =>
|
||||
@@ -622,22 +572,11 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
_logger.End();
|
||||
|
||||
if (!DeferOutcomeConclusion)
|
||||
{
|
||||
UpdateGlobalStepsContext();
|
||||
}
|
||||
UpdateGlobalStepsContext();
|
||||
|
||||
return Result.Value;
|
||||
}
|
||||
|
||||
public void FlushDeferredOutcomeConclusion()
|
||||
{
|
||||
if (DeferOutcomeConclusion)
|
||||
{
|
||||
UpdateGlobalStepsContext();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateGlobalStepsContext()
|
||||
{
|
||||
// Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name.
|
||||
@@ -713,40 +652,6 @@ namespace GitHub.Runner.Worker
|
||||
Global.StepsContext.SetOutput(ScopeName, ContextName, name, value, out reference);
|
||||
}
|
||||
|
||||
public void FlushDeferredOutputs()
|
||||
{
|
||||
if (DeferredOutputs == null || DeferredOutputs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var kvp in DeferredOutputs)
|
||||
{
|
||||
Global.StepsContext.SetOutput(ScopeName, ContextName, kvp.Key, kvp.Value, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushDeferredEnvironment()
|
||||
{
|
||||
if (DeferredEnvironmentVariables != null)
|
||||
{
|
||||
foreach (var kvp in DeferredEnvironmentVariables)
|
||||
{
|
||||
Global.EnvironmentVariables[kvp.Key] = kvp.Value;
|
||||
SetEnvContext(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (DeferredPrependPath != null)
|
||||
{
|
||||
foreach (var path in DeferredPrependPath)
|
||||
{
|
||||
Global.PrependPath.RemoveAll(x => string.Equals(x, path, StringComparison.CurrentCulture));
|
||||
Global.PrependPath.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTimeout(TimeSpan? timeout)
|
||||
{
|
||||
if (timeout != null)
|
||||
@@ -1443,10 +1348,7 @@ namespace GitHub.Runner.Worker
|
||||
Trace.Info($"Updated step result (continue on error)");
|
||||
}
|
||||
|
||||
if (!DeferOutcomeConclusion)
|
||||
{
|
||||
UpdateGlobalStepsContext();
|
||||
}
|
||||
UpdateGlobalStepsContext();
|
||||
}
|
||||
|
||||
internal IPipelineTemplateEvaluator ToPipelineTemplateEvaluatorInternal(bool allowServiceContainerCommand, ObjectTemplating.ITraceWriter traceWriter = null)
|
||||
|
||||
@@ -122,16 +122,8 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (context.DeferredPrependPath != null)
|
||||
{
|
||||
context.DeferredPrependPath.RemoveAll(x => string.Equals(x, line, StringComparison.CurrentCulture));
|
||||
context.DeferredPrependPath.Add(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Global.PrependPath.RemoveAll(x => string.Equals(x, line, StringComparison.CurrentCulture));
|
||||
context.Global.PrependPath.Add(line);
|
||||
}
|
||||
context.Global.PrependPath.RemoveAll(x => string.Equals(x, line, StringComparison.CurrentCulture));
|
||||
context.Global.PrependPath.Add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,15 +172,8 @@ namespace GitHub.Runner.Worker
|
||||
string name,
|
||||
string value)
|
||||
{
|
||||
if (context.DeferredEnvironmentVariables != null)
|
||||
{
|
||||
context.DeferredEnvironmentVariables[name] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Global.EnvironmentVariables[name] = value;
|
||||
context.SetEnvContext(name, value);
|
||||
}
|
||||
context.Global.EnvironmentVariables[name] = value;
|
||||
context.SetEnvContext(name, value);
|
||||
context.Debug($"{name}='{value}'");
|
||||
}
|
||||
|
||||
@@ -317,14 +302,7 @@ namespace GitHub.Runner.Worker
|
||||
var pairs = new EnvFileKeyValuePairs(context, filePath);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
if (context.DeferredOutputs != null)
|
||||
{
|
||||
context.DeferredOutputs[pair.Key] = pair.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.SetOutput(pair.Key, pair.Value, out var reference);
|
||||
}
|
||||
context.SetOutput(pair.Key, pair.Value, out var reference);
|
||||
context.Debug($"Set output {pair.Key} = {pair.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,38 +345,6 @@ namespace GitHub.Runner.Worker
|
||||
preJobSteps.Add(preStep);
|
||||
}
|
||||
}
|
||||
else if (step.Type == Pipelines.StepType.BackgroundStepControl)
|
||||
{
|
||||
var ctrl = step as Pipelines.BackgroundStepControl;
|
||||
Trace.Info($"Adding {ctrl.ControlType} step for: {string.Join(", ", ctrl.StepIds ?? Array.Empty<string>())}");
|
||||
var controlType = ctrl.ControlType;
|
||||
if (string.IsNullOrEmpty(controlType))
|
||||
{
|
||||
throw new ArgumentException($"Background step control '{step.Name}' has no control type.");
|
||||
}
|
||||
if (controlType != Pipelines.BackgroundControlTypes.Wait &&
|
||||
controlType != Pipelines.BackgroundControlTypes.WaitAll &&
|
||||
controlType != Pipelines.BackgroundControlTypes.Cancel)
|
||||
{
|
||||
throw new ArgumentException($"Unknown background step control type '{controlType}' for step '{step.Name}'.");
|
||||
}
|
||||
var displayName = (ctrl.DisplayNameToken as GitHub.DistributedTask.ObjectTemplating.Tokens.StringToken)?.Value
|
||||
?? step.DisplayName ?? step.Name ?? ctrl.ControlType;
|
||||
var data = new BackgroundStepControlFlowData
|
||||
{
|
||||
Type = controlType,
|
||||
StepId = step.Id,
|
||||
StepName = step.Name,
|
||||
StepIds = ctrl.StepIds,
|
||||
ParallelGroupId = ctrl.ParallelGroupId,
|
||||
};
|
||||
var bgCoord = HostContext.GetService<IBackgroundStepCoordinator>();
|
||||
jobSteps.Add(new JobExtensionRunner(
|
||||
runAsync: bgCoord.RunControlFlowAsync,
|
||||
condition: $"{PipelineTemplateConstants.Always}()",
|
||||
displayName: displayName,
|
||||
data: data));
|
||||
}
|
||||
}
|
||||
|
||||
if (message.Variables.TryGetValue("system.workflowFileFullPath", out VariableValue workflowFileFullPath))
|
||||
@@ -432,107 +400,13 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
// Create execution context for job steps
|
||||
// Build mapping of logical step ID (ContextName) → external ID (timeline record GUID)
|
||||
// so wait/cancel steps can reference background steps by external ID.
|
||||
var contextNameToExternalId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var hasBackgroundSteps = false;
|
||||
var backgroundStepExternalIds = new List<string>();
|
||||
|
||||
// Track which background steps are explicitly covered by wait/wait-all/cancel
|
||||
var coveredBackgroundIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var step in jobSteps)
|
||||
{
|
||||
if (step is IActionRunner actionStep)
|
||||
{
|
||||
ArgUtil.NotNull(actionStep, step.DisplayName);
|
||||
intraActionStates.TryGetValue(actionStep.Action.Id, out var intraActionState);
|
||||
|
||||
var isBg = actionStep.Action?.Background == true;
|
||||
actionStep.ExecutionContext = jobContext.CreateChild(
|
||||
actionStep.Action.Id, actionStep.DisplayName, actionStep.Action.Name,
|
||||
null, actionStep.Action.ContextName, ActionRunStage.Main, intraActionState,
|
||||
isBackground: isBg,
|
||||
parallelGroupId: isBg ? actionStep.Action.ParallelGroupId : null);
|
||||
|
||||
if (isBg)
|
||||
{
|
||||
hasBackgroundSteps = true;
|
||||
var externalId = actionStep.Action.Id.ToString("N");
|
||||
contextNameToExternalId[actionStep.Action.ContextName] = externalId;
|
||||
backgroundStepExternalIds.Add(externalId);
|
||||
}
|
||||
}
|
||||
else if (step is JobExtensionRunner runnerStep && runnerStep.Data is BackgroundStepControlFlowData cf)
|
||||
{
|
||||
// Resolve step IDs to external IDs and track coverage
|
||||
string[] externalIds = null;
|
||||
if (cf.StepIds != null && cf.StepIds.Length > 0)
|
||||
{
|
||||
foreach (var id in cf.StepIds)
|
||||
{
|
||||
coveredBackgroundIds.Add(id);
|
||||
}
|
||||
externalIds = cf.StepIds
|
||||
.Where(id => contextNameToExternalId.ContainsKey(id))
|
||||
.Select(id => contextNameToExternalId[id])
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (cf.Type == Pipelines.BackgroundControlTypes.WaitAll)
|
||||
{
|
||||
externalIds = backgroundStepExternalIds.Count > 0 ? backgroundStepExternalIds.ToArray() : null;
|
||||
foreach (var id in contextNameToExternalId.Keys)
|
||||
{
|
||||
coveredBackgroundIds.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
step.ExecutionContext = jobContext.CreateChild(
|
||||
cf.StepId, step.DisplayName, cf.StepName,
|
||||
null, cf.StepName, ActionRunStage.Main,
|
||||
backgroundControlType: cf.Type,
|
||||
backgroundControlStepIds: externalIds,
|
||||
parallelGroupId: cf.ParallelGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
// Add implicit wait-all only if there are background steps not covered by any wait/wait-all/cancel
|
||||
var allBackgroundIds = contextNameToExternalId.Keys;
|
||||
var hasUncoveredBackgroundSteps = allBackgroundIds.Any(id => !coveredBackgroundIds.Contains(id));
|
||||
if (hasBackgroundSteps)
|
||||
{
|
||||
// Initialize coordinator only when there are background steps
|
||||
var bgCoordinator = HostContext.GetService<IBackgroundStepCoordinator>();
|
||||
var maxBgSteps = jobContext.Global.Variables.GetInt("system.runner.maxbackgroundsteps");
|
||||
var maxConcurrent = (maxBgSteps.HasValue && maxBgSteps.Value > 0) ? maxBgSteps.Value : 10;
|
||||
bgCoordinator.InitializeCoordinator(maxConcurrent);
|
||||
|
||||
// Add implicit wait-all only if there are uncovered background steps
|
||||
if (hasUncoveredBackgroundSteps)
|
||||
{
|
||||
var implicitStepId = Guid.NewGuid();
|
||||
var implicitWaitAllData = new BackgroundStepControlFlowData
|
||||
{
|
||||
Type = Pipelines.BackgroundControlTypes.WaitAll,
|
||||
StepId = implicitStepId,
|
||||
StepName = "__implicit_wait_all",
|
||||
};
|
||||
var implicitWaitAll = new JobExtensionRunner(
|
||||
runAsync: bgCoordinator.RunControlFlowAsync,
|
||||
condition: $"{PipelineTemplateConstants.Always}()",
|
||||
displayName: "Wait for all background steps",
|
||||
data: implicitWaitAllData);
|
||||
var uncoveredExternalIds = contextNameToExternalId
|
||||
.Where(kvp => !coveredBackgroundIds.Contains(kvp.Key))
|
||||
.Select(kvp => kvp.Value)
|
||||
.ToArray();
|
||||
implicitWaitAll.ExecutionContext = jobContext.CreateChild(
|
||||
implicitStepId, implicitWaitAll.DisplayName, "__implicit_wait_all",
|
||||
null, "__implicit_wait_all", ActionRunStage.Main,
|
||||
backgroundControlType: Pipelines.BackgroundControlTypes.WaitAll,
|
||||
backgroundControlStepIds: uncoveredExternalIds.Length > 0 ? uncoveredExternalIds : null);
|
||||
jobSteps.Add(implicitWaitAll);
|
||||
actionStep.ExecutionContext = jobContext.CreateChild(actionStep.Action.Id, actionStep.DisplayName, actionStep.Action.Name, null, actionStep.Action.ContextName, ActionRunStage.Main, intraActionState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||
<PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.48" />
|
||||
<PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.39" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
private static readonly Regex _propertyRegex = new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
|
||||
private readonly DictionaryContextData _contextData = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clears memory for a composite action's isolated "steps" context, after the action
|
||||
@@ -26,12 +25,9 @@ namespace GitHub.Runner.Worker
|
||||
/// </summary>
|
||||
public void ClearScope(string scopeName)
|
||||
{
|
||||
lock (_lock)
|
||||
if (_contextData.TryGetValue(scopeName, out _))
|
||||
{
|
||||
if (_contextData.TryGetValue(scopeName, out _))
|
||||
{
|
||||
_contextData[scopeName] = new DictionaryContextData();
|
||||
}
|
||||
_contextData[scopeName] = new DictionaryContextData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,26 +41,23 @@ namespace GitHub.Runner.Worker
|
||||
/// </summary>
|
||||
public DictionaryContextData GetScope(string scopeName)
|
||||
{
|
||||
lock (_lock)
|
||||
if (scopeName == null)
|
||||
{
|
||||
if (scopeName == null)
|
||||
{
|
||||
scopeName = string.Empty;
|
||||
}
|
||||
|
||||
var scope = default(DictionaryContextData);
|
||||
if (_contextData.TryGetValue(scopeName, out var scopeValue))
|
||||
{
|
||||
scope = scopeValue.AssertDictionary("scope");
|
||||
}
|
||||
else
|
||||
{
|
||||
scope = new DictionaryContextData();
|
||||
_contextData.Add(scopeName, scope);
|
||||
}
|
||||
|
||||
return scope;
|
||||
scopeName = string.Empty;
|
||||
}
|
||||
|
||||
var scope = default(DictionaryContextData);
|
||||
if (_contextData.TryGetValue(scopeName, out var scopeValue))
|
||||
{
|
||||
scope = scopeValue.AssertDictionary("scope");
|
||||
}
|
||||
else
|
||||
{
|
||||
scope = new DictionaryContextData();
|
||||
_contextData.Add(scopeName, scope);
|
||||
}
|
||||
|
||||
return scope;
|
||||
}
|
||||
|
||||
public void SetOutput(
|
||||
@@ -74,19 +67,16 @@ namespace GitHub.Runner.Worker
|
||||
string value,
|
||||
out string reference)
|
||||
{
|
||||
lock (_lock)
|
||||
var step = GetStep(scopeName, stepName);
|
||||
var outputs = step["outputs"].AssertDictionary("outputs");
|
||||
outputs[outputName] = new StringContextData(value);
|
||||
if (_propertyRegex.IsMatch(outputName))
|
||||
{
|
||||
var step = GetStep(scopeName, stepName);
|
||||
var outputs = step["outputs"].AssertDictionary("outputs");
|
||||
outputs[outputName] = new StringContextData(value);
|
||||
if (_propertyRegex.IsMatch(outputName))
|
||||
{
|
||||
reference = $"steps.{stepName}.outputs.{outputName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
reference = $"steps['{stepName}']['outputs']['{outputName}']";
|
||||
}
|
||||
reference = $"steps.{stepName}.outputs.{outputName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
reference = $"steps['{stepName}']['outputs']['{outputName}']";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,11 +85,8 @@ namespace GitHub.Runner.Worker
|
||||
string stepName,
|
||||
ActionResult conclusion)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var step = GetStep(scopeName, stepName);
|
||||
step["conclusion"] = new StringContextData(conclusion.ToString().ToLowerInvariant());
|
||||
}
|
||||
var step = GetStep(scopeName, stepName);
|
||||
step["conclusion"] = new StringContextData(conclusion.ToString().ToLowerInvariant());
|
||||
}
|
||||
|
||||
public void SetOutcome(
|
||||
@@ -107,11 +94,8 @@ namespace GitHub.Runner.Worker
|
||||
string stepName,
|
||||
ActionResult outcome)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var step = GetStep(scopeName, stepName);
|
||||
step["outcome"] = new StringContextData(outcome.ToString().ToLowerInvariant());
|
||||
}
|
||||
var step = GetStep(scopeName, stepName);
|
||||
step["outcome"] = new StringContextData(outcome.ToString().ToLowerInvariant());
|
||||
}
|
||||
|
||||
private DictionaryContextData GetStep(string scopeName, string stepName)
|
||||
|
||||
@@ -41,8 +41,6 @@ namespace GitHub.Runner.Worker
|
||||
ArgUtil.NotNull(jobContext, nameof(jobContext));
|
||||
ArgUtil.NotNull(jobContext.JobSteps, nameof(jobContext.JobSteps));
|
||||
|
||||
var _bgCoordinator = HostContext.GetService<IBackgroundStepCoordinator>();
|
||||
|
||||
// TaskResult:
|
||||
// Abandoned (Server set this.)
|
||||
// Canceled
|
||||
@@ -59,15 +57,6 @@ namespace GitHub.Runner.Worker
|
||||
if (jobContext.JobSteps.Count == 0 && !checkPostJobActions)
|
||||
{
|
||||
checkPostJobActions = true;
|
||||
|
||||
// Safety net: wait for any unwaited background steps before post-hooks
|
||||
var backgroundResult = await _bgCoordinator.WaitForUnwaitedStepsAsync(jobContext.CancellationToken);
|
||||
if (backgroundResult != TaskResult.Succeeded)
|
||||
{
|
||||
jobContext.Result = TaskResultUtil.MergeTaskResults(jobContext.Result, backgroundResult);
|
||||
jobContext.JobContext.Status = jobContext.Result?.ToActionResult();
|
||||
}
|
||||
|
||||
while (jobContext.PostJobSteps.TryPop(out var postStep))
|
||||
{
|
||||
jobContext.JobSteps.Enqueue(postStep);
|
||||
@@ -83,11 +72,8 @@ namespace GitHub.Runner.Worker
|
||||
ArgUtil.NotNull(step.ExecutionContext.Global, nameof(step.ExecutionContext.Global));
|
||||
ArgUtil.NotNull(step.ExecutionContext.Global.Variables, nameof(step.ExecutionContext.Global.Variables));
|
||||
|
||||
// Start — defer for background steps until the slot is acquired
|
||||
if (!step.ExecutionContext.IsBackground)
|
||||
{
|
||||
step.ExecutionContext.Start();
|
||||
}
|
||||
// Start
|
||||
step.ExecutionContext.Start();
|
||||
|
||||
// Expression functions
|
||||
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<AlwaysFunction>(PipelineTemplateConstants.Always, 0, 0));
|
||||
@@ -242,22 +228,14 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
else
|
||||
{
|
||||
if (step.ExecutionContext.IsBackground)
|
||||
{
|
||||
// Queue the background step via coordinator
|
||||
_bgCoordinator.StartBackgroundStep(step, jobContext.CancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pause for DAP debugger before step execution
|
||||
await dapDebugger?.OnStepStartingAsync(step);
|
||||
// Pause for DAP debugger before step execution
|
||||
await dapDebugger?.OnStepStartingAsync(step);
|
||||
|
||||
// Run the step synchronously (normal behavior)
|
||||
await RunStepAsync(step, jobContext.CancellationToken);
|
||||
CompleteStep(step);
|
||||
// Run the step
|
||||
await RunStepAsync(step, jobContext.CancellationToken);
|
||||
CompleteStep(step);
|
||||
|
||||
dapDebugger?.OnStepCompleted(step);
|
||||
}
|
||||
dapDebugger?.OnStepCompleted(step);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
Inputs = actionToClone.Inputs?.Clone();
|
||||
ContextName = actionToClone?.ContextName;
|
||||
DisplayNameToken = actionToClone.DisplayNameToken?.Clone();
|
||||
Background = actionToClone.Background;
|
||||
}
|
||||
|
||||
public override StepType Type => StepType.Action;
|
||||
@@ -50,9 +49,6 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken Inputs { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public bool Background { get; set; }
|
||||
|
||||
public override Step Clone()
|
||||
{
|
||||
return new ActionStep(this);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
/// <summary>
|
||||
/// Known control-flow types for background step control steps.
|
||||
/// Wire values must match run-service constants (wait, wait-all, cancel).
|
||||
/// </summary>
|
||||
public static class BackgroundControlTypes
|
||||
{
|
||||
public const string Wait = "wait";
|
||||
public const string WaitAll = "wait-all";
|
||||
public const string Cancel = "cancel";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a unified background step control-flow step (wait, wait-all, cancel).
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public class BackgroundStepControl : JobStep
|
||||
{
|
||||
[JsonConstructor]
|
||||
public BackgroundStepControl()
|
||||
{
|
||||
}
|
||||
|
||||
private BackgroundStepControl(BackgroundStepControl stepToClone)
|
||||
: base(stepToClone)
|
||||
{
|
||||
this.ControlType = stepToClone.ControlType;
|
||||
this.StepIds = stepToClone.StepIds != null
|
||||
? (string[])stepToClone.StepIds.Clone()
|
||||
: null;
|
||||
this.DisplayNameToken = stepToClone.DisplayNameToken?.Clone();
|
||||
}
|
||||
|
||||
public override StepType Type => StepType.BackgroundStepControl;
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string ControlType { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string[] StepIds { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public TemplateToken DisplayNameToken { get; set; }
|
||||
|
||||
public override Step Clone()
|
||||
{
|
||||
return new BackgroundStepControl(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
this.Condition = stepToClone.Condition;
|
||||
this.ContinueOnError = stepToClone.ContinueOnError?.Clone();
|
||||
this.TimeoutInMinutes = stepToClone.TimeoutInMinutes?.Clone();
|
||||
this.ParallelGroupId = stepToClone.ParallelGroupId;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
@@ -45,8 +44,5 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string ParallelGroupId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataContract]
|
||||
[KnownType(typeof(ActionStep))]
|
||||
[KnownType(typeof(BackgroundStepControl))]
|
||||
[JsonConverter(typeof(StepConverter))]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public abstract class Step
|
||||
@@ -69,7 +68,5 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
{
|
||||
[DataMember]
|
||||
Action = 4,
|
||||
[DataMember]
|
||||
BackgroundStepControl = 5,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,6 @@ namespace GitHub.DistributedTask.Pipelines
|
||||
case StepType.Action:
|
||||
stepObject = new ActionStep();
|
||||
break;
|
||||
case StepType.BackgroundStepControl:
|
||||
stepObject = new BackgroundStepControl();
|
||||
break;
|
||||
}
|
||||
|
||||
using (var objectReader = value.CreateReader())
|
||||
|
||||
@@ -43,10 +43,6 @@ namespace GitHub.DistributedTask.WebApi
|
||||
this.WarningCount = recordToBeCloned.WarningCount;
|
||||
this.NoticeCount = recordToBeCloned.NoticeCount;
|
||||
this.AgentPlatform = recordToBeCloned.AgentPlatform;
|
||||
this.IsBackground = recordToBeCloned.IsBackground;
|
||||
this.BackgroundControlType = recordToBeCloned.BackgroundControlType;
|
||||
this.BackgroundControlStepIds = recordToBeCloned.BackgroundControlStepIds;
|
||||
this.ParallelGroupId = recordToBeCloned.ParallelGroupId;
|
||||
|
||||
if (recordToBeCloned.Log != null)
|
||||
{
|
||||
@@ -293,34 +289,6 @@ namespace GitHub.DistributedTask.WebApi
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(Order = 140, EmitDefaultValue = false)]
|
||||
public bool IsBackground
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(Order = 141, EmitDefaultValue = false)]
|
||||
public string BackgroundControlType
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(Order = 142, EmitDefaultValue = false)]
|
||||
public string[] BackgroundControlStepIds
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DataMember(Order = 144, EmitDefaultValue = false)]
|
||||
public string ParallelGroupId
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public IList<TimelineAttempt> PreviousAttempts
|
||||
{
|
||||
get
|
||||
|
||||
@@ -50,14 +50,5 @@ namespace GitHub.Actions.RunService.WebApi
|
||||
|
||||
[DataMember(Name = "annotations", EmitDefaultValue = false)]
|
||||
public List<Annotation> Annotations { get; set; }
|
||||
|
||||
[DataMember(Name = "is_background", EmitDefaultValue = false)]
|
||||
public bool IsBackground { get; set; }
|
||||
|
||||
[DataMember(Name = "background_control_type", EmitDefaultValue = false)]
|
||||
public string BackgroundControlType { get; set; }
|
||||
|
||||
[DataMember(Name = "background_control_step_ids", EmitDefaultValue = false)]
|
||||
public string[] BackgroundControlStepIds { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.7" />
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.6" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
|
||||
<PackageReference Include="Minimatch" Version="2.0.0" />
|
||||
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="10.0.7" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="10.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -179,14 +179,6 @@ namespace GitHub.Services.Results.Contracts
|
||||
public string CompletedAt;
|
||||
[DataMember]
|
||||
public Conclusion Conclusion;
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public bool IsBackground;
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string BackgroundControlType;
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string[] BackgroundControlStepIds;
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string ParallelGroupId;
|
||||
}
|
||||
|
||||
public enum Status
|
||||
|
||||
@@ -514,7 +514,7 @@ namespace GitHub.Services.Results.Client
|
||||
|
||||
private Step ConvertTimelineRecordToStep(TimelineRecord r)
|
||||
{
|
||||
var step = new Step()
|
||||
return new Step()
|
||||
{
|
||||
ExternalId = r.Id.ToString(),
|
||||
Number = r.Order.GetValueOrDefault(),
|
||||
@@ -522,25 +522,8 @@ namespace GitHub.Services.Results.Client
|
||||
Status = ConvertStateToStatus(r.State.GetValueOrDefault()),
|
||||
StartedAt = r.StartTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
|
||||
CompletedAt = r.FinishTime?.ToString(Constants.TimestampFormat, CultureInfo.InvariantCulture),
|
||||
Conclusion = ConvertResultToConclusion(r.Result),
|
||||
IsBackground = r.IsBackground,
|
||||
Conclusion = ConvertResultToConclusion(r.Result)
|
||||
};
|
||||
|
||||
// Set background control type directly (no enum mapping needed)
|
||||
if (!string.IsNullOrEmpty(r.BackgroundControlType))
|
||||
{
|
||||
step.BackgroundControlType = r.BackgroundControlType;
|
||||
}
|
||||
if (r.BackgroundControlStepIds != null)
|
||||
{
|
||||
step.BackgroundControlStepIds = r.BackgroundControlStepIds;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(r.ParallelGroupId))
|
||||
{
|
||||
step.ParallelGroupId = r.ParallelGroupId;
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
private Status ConvertStateToStatus(TimelineRecordState s)
|
||||
|
||||
@@ -1,702 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using GitHub.DistributedTask.Expressions2;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class BackgroundStepsL0
|
||||
{
|
||||
private Mock<IExecutionContext> _ec;
|
||||
private StepsRunner _stepsRunner;
|
||||
private Variables _variables;
|
||||
private Dictionary<string, string> _env;
|
||||
private DictionaryContextData _contexts;
|
||||
private JobContext _jobContext;
|
||||
private StepsContext _stepContext;
|
||||
|
||||
private TestHostContext CreateTestContext([CallerMemberName] String testName = "")
|
||||
{
|
||||
var hc = new TestHostContext(this, testName);
|
||||
Dictionary<string, VariableValue> variablesToCopy = new();
|
||||
_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.Global).Returns(new GlobalContext { WriteDebug = true });
|
||||
_ec.Object.Global.Variables = _variables;
|
||||
_ec.Object.Global.EnvironmentVariables = _env;
|
||||
_ec.Object.Global.FileTable = new List<string>();
|
||||
|
||||
_contexts = new DictionaryContextData();
|
||||
_jobContext = new JobContext();
|
||||
_contexts["github"] = new GitHubContext();
|
||||
_contexts["runner"] = new DictionaryContextData();
|
||||
_contexts["job"] = _jobContext;
|
||||
_ec.Setup(x => x.ExpressionValues).Returns(_contexts);
|
||||
_ec.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
|
||||
_ec.Setup(x => x.JobContext).Returns(_jobContext);
|
||||
_ec.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||
|
||||
_stepContext = new StepsContext();
|
||||
_ec.Object.Global.StepsContext = _stepContext;
|
||||
|
||||
_ec.Setup(x => x.PostJobSteps).Returns(new Stack<IStep>());
|
||||
|
||||
var trace = hc.GetTrace();
|
||||
|
||||
// Mock CreateChild for implicit wait-all step injection
|
||||
_ec.Setup(x => x.CreateChild(
|
||||
It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ActionRunStage>(),
|
||||
It.IsAny<Dictionary<string, string>>(), It.IsAny<int?>(), It.IsAny<IPagingLogger>(),
|
||||
It.IsAny<bool>(), It.IsAny<List<Issue>>(), It.IsAny<CancellationTokenSource>(),
|
||||
It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<TimeSpan?>(),
|
||||
It.IsAny<bool>(), It.IsAny<string>(), It.IsAny<string[]>(), It.IsAny<string>()))
|
||||
.Returns((Guid recordId, string displayName, string refName, string scopeName, string contextName,
|
||||
ActionRunStage stage, Dictionary<string, string> intraActionState, int? recordOrder, IPagingLogger logger,
|
||||
bool isEmbedded, List<Issue> issues, CancellationTokenSource cts, Guid embeddedId, string siblingScopeName, TimeSpan? timeout,
|
||||
bool isBackground, string backgroundControlType, string[] backgroundControlStepIds, string parallelGroupId) =>
|
||||
{
|
||||
var childEc = new Mock<IExecutionContext>();
|
||||
childEc.SetupAllProperties();
|
||||
childEc.Setup(x => x.Global).Returns(() => _ec.Object.Global);
|
||||
childEc.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());
|
||||
childEc.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
|
||||
childEc.Setup(x => x.ContextName).Returns(contextName);
|
||||
childEc.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||
childEc.Setup(x => x.Complete(It.IsAny<TaskResult?>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((TaskResult? r, string currentOperation, string resultCode) =>
|
||||
{
|
||||
if (r != null) childEc.Object.Result = r;
|
||||
});
|
||||
childEc.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { trace.Info($"[{tag}]{message}"); });
|
||||
return childEc.Object;
|
||||
});
|
||||
|
||||
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { trace.Info($"[{tag}]{message}"); });
|
||||
|
||||
_stepsRunner = new StepsRunner();
|
||||
_stepsRunner.Initialize(hc);
|
||||
|
||||
var bgCoordinator = new BackgroundStepCoordinator();
|
||||
bgCoordinator.Initialize(hc);
|
||||
hc.SetSingleton<IBackgroundStepCoordinator>(bgCoordinator);
|
||||
|
||||
var mockDapDebugger = new Mock<IDapDebugger>();
|
||||
hc.SetSingleton(mockDapDebugger.Object);
|
||||
|
||||
return hc;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task BackgroundStepRunsConcurrentlyWithForeground()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: background step that takes time, followed by a foreground step
|
||||
var executionOrder = new List<string>();
|
||||
|
||||
var bgStep = CreateStep(hc, TaskResult.Succeeded, "success()", name: "bg-step", contextName: "bg", isBackground: true);
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(async () =>
|
||||
{
|
||||
executionOrder.Add("bg-start");
|
||||
await Task.Delay(2000);
|
||||
executionOrder.Add("bg-end");
|
||||
});
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "bg-step",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "bg",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var fgStep = CreateStep(hc, TaskResult.Succeeded, "success()", name: "fg-step", contextName: "fg");
|
||||
fgStep.Setup(x => x.RunAsync()).Returns(() =>
|
||||
{
|
||||
executionOrder.Add("fg-run");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
var waitAllStep = CreateWaitAllStep(hc);
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, fgStep.Object, waitAllStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: foreground step should start before background step finishes
|
||||
Assert.Contains("bg-start", executionOrder);
|
||||
Assert.Contains("fg-run", executionOrder);
|
||||
Assert.Contains("bg-end", executionOrder);
|
||||
var fgIndex = executionOrder.IndexOf("fg-run");
|
||||
var bgEndIndex = executionOrder.IndexOf("bg-end");
|
||||
Assert.True(fgIndex < bgEndIndex, "Foreground step should run before background step completes");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task WaitStepBlocksUntilBackgroundCompletes()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange
|
||||
var bgCompleted = false;
|
||||
|
||||
var bgStep = CreateStep(hc, TaskResult.Succeeded, "success()", name: "db", contextName: "db", isBackground: true);
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
bgCompleted = true;
|
||||
});
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "db",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "db",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var waitStep = CreateWaitStep(hc, new[] { "db" });
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, waitStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: background step must have completed after wait
|
||||
Assert.True(bgCompleted, "Background step should have completed after wait");
|
||||
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task BackgroundStepFailurePropagatesAtWait()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: background step that fails
|
||||
var bgStep = CreateStep(hc, TaskResult.Failed, "success()", name: "flaky", contextName: "flaky", isBackground: true);
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(() =>
|
||||
{
|
||||
throw new Exception("Service crashed");
|
||||
});
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "flaky",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "flaky",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var waitStep = CreateWaitStep(hc, new[] { "flaky" });
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, waitStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: job should fail because background step failed
|
||||
Assert.Equal(TaskResult.Failed, _ec.Object.Result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CancelStepTerminatesBackgroundStep()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: background step that runs until cancelled via ExecutionContext.CancellationToken
|
||||
var stepCts = new CancellationTokenSource();
|
||||
|
||||
var bgStep = CreateStep(hc, TaskResult.Succeeded, "success()", name: "server", contextName: "server");
|
||||
// Wire CancellationToken to our CTS so the cancel path can trigger it
|
||||
var bgStepContext = Mock.Get(bgStep.Object.ExecutionContext);
|
||||
bgStepContext.Setup(x => x.CancellationToken).Returns(stepCts.Token);
|
||||
bgStepContext.Setup(x => x.CancelToken()).Callback(() => stepCts.Cancel());
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stepCts.Token);
|
||||
});
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "server",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "server",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var cancelStep = CreateCancelStep(hc, "server");
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, cancelStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: background step should have been cancelled
|
||||
// Note: the cancel mechanism uses the BackgroundStepContext.Cts, not bgCts
|
||||
// so wasCancelled may not be true in this mock, but the step should complete
|
||||
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task WaitAllWaitsForAllBackgroundSteps()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: two background steps
|
||||
var step1Done = false;
|
||||
var step2Done = false;
|
||||
|
||||
var bgStep1 = CreateStep(hc, TaskResult.Succeeded, "success()", name: "svc1", contextName: "svc1", isBackground: true);
|
||||
bgStep1.Setup(x => x.RunAsync()).Returns(async () =>
|
||||
{
|
||||
await Task.Delay(50);
|
||||
step1Done = true;
|
||||
});
|
||||
bgStep1.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "svc1",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "svc1",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var bgStep2 = CreateStep(hc, TaskResult.Succeeded, "success()", name: "svc2", contextName: "svc2", isBackground: true);
|
||||
bgStep2.Setup(x => x.RunAsync()).Returns(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
step2Done = true;
|
||||
});
|
||||
bgStep2.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "svc2",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "svc2",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var waitAllStep = CreateWaitAllStep(hc);
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep1.Object, bgStep2.Object, waitAllStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert
|
||||
Assert.True(step1Done, "Background step 1 should have completed");
|
||||
Assert.True(step2Done, "Background step 2 should have completed");
|
||||
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CancelStepPublishesCanceledBackgroundExternalId()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
var bgStep = CreateStep(hc, TaskResult.Succeeded, "success()", name: "server", contextName: "server", isBackground: true);
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(Task.CompletedTask);
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "server",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "server",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var cancelStep = CreateCancelStep(hc, "server");
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, cancelStep
|
||||
}));
|
||||
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: cancel step completed without error
|
||||
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task CanceledBackgroundStepDoesNotAffectJobResult()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: a background step that runs until explicitly canceled. When canceled it
|
||||
// reports TaskResult.Canceled, but since the cancellation is expected (driven by a
|
||||
// cancel control step), it must not impact the overall job result.
|
||||
using var stepCts = new CancellationTokenSource();
|
||||
|
||||
var bgStep = CreateStep(hc, TaskResult.Succeeded, "success()", name: "server", contextName: "server", isBackground: true);
|
||||
var bgStepContext = Mock.Get(bgStep.Object.ExecutionContext);
|
||||
bgStepContext.Setup(x => x.CancellationToken).Returns(stepCts.Token);
|
||||
bgStepContext.Setup(x => x.CancelToken()).Callback(() => stepCts.Cancel());
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), stepCts.Token);
|
||||
});
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "server",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "server",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var cancelStep = CreateCancelStep(hc, "server");
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, cancelStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: the canceled background step reported Canceled, but the job result is unaffected.
|
||||
Assert.Equal(TaskResult.Canceled, bgStep.Object.ExecutionContext.Result);
|
||||
Assert.Equal(TaskResult.Succeeded, _ec.Object.Result ?? TaskResult.Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task FailedBackgroundStepTargetedByCancelStillAffectsJobResult()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: a background step that fails (e.g. before the cancel takes effect). Even
|
||||
// though a cancel control step targets it, its Failed result must still propagate to
|
||||
// the overall job result.
|
||||
var bgStep = CreateStep(hc, TaskResult.Failed, "success()", name: "server", contextName: "server", isBackground: true);
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(Task.CompletedTask);
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "server",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "server",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var cancelStep = CreateCancelStep(hc, "server");
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, cancelStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: the background step failed, so the job result reflects that failure.
|
||||
Assert.Equal(TaskResult.Failed, bgStep.Object.ExecutionContext.Result);
|
||||
Assert.Equal(TaskResult.Failed, _ec.Object.Result);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StepsContextThreadSafety()
|
||||
{
|
||||
// Test that concurrent SetOutput/SetConclusion doesn't throw
|
||||
var stepsContext = new StepsContext();
|
||||
var tasks = new List<Task>();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var index = i;
|
||||
tasks.Add(Task.Run(() =>
|
||||
{
|
||||
stepsContext.SetOutput("", $"step{index}", "out", $"value{index}", out _);
|
||||
stepsContext.SetConclusion("", $"step{index}", ActionResult.Success);
|
||||
stepsContext.SetOutcome("", $"step{index}", ActionResult.Success);
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Assert: all 100 steps should have their data set
|
||||
var scope = stepsContext.GetScope("");
|
||||
Assert.Equal(100, scope.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task ControlFlowStepsRunEvenAfterFailure()
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
// Arrange: a background step, a foreground step that fails, then a wait step
|
||||
var bgStep = CreateStep(hc, TaskResult.Succeeded, "success()", name: "bg", contextName: "bg", isBackground: true);
|
||||
bgStep.Setup(x => x.RunAsync()).Returns(Task.CompletedTask);
|
||||
bgStep.Setup(x => x.Action).Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = "bg",
|
||||
Id = Guid.NewGuid(),
|
||||
ContextName = "bg",
|
||||
Background = true,
|
||||
});
|
||||
|
||||
var failStep = CreateStep(hc, TaskResult.Failed, "success()", name: "fail", contextName: "fail");
|
||||
|
||||
// Wait step uses always() condition — should run even after failure
|
||||
var waitStep = CreateWaitStep(hc, new[] { "bg" });
|
||||
waitStep.Condition = $"{GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants.Always}()";
|
||||
|
||||
_ec.Object.Result = null;
|
||||
_ec.Setup(x => x.JobSteps).Returns(new Queue<IStep>(new IStep[]
|
||||
{
|
||||
bgStep.Object, failStep.Object, waitStep
|
||||
}));
|
||||
|
||||
// Act
|
||||
await _stepsRunner.RunAsync(jobContext: _ec.Object);
|
||||
|
||||
// Assert: wait step should have run (not skipped) because it has always() condition
|
||||
Assert.NotNull(waitStep.ExecutionContext.Result);
|
||||
Assert.NotEqual(TaskResult.Skipped, waitStep.ExecutionContext.Result);
|
||||
}
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private Mock<IActionRunner> CreateStep(TestHostContext hc, TaskResult result, string condition, string name = "Test", string contextName = null, Guid? recordId = null, bool isBackground = false)
|
||||
{
|
||||
var stepRecordId = recordId ?? Guid.NewGuid();
|
||||
var step = new Mock<IActionRunner>();
|
||||
step.Setup(x => x.Condition).Returns(condition);
|
||||
step.Setup(x => x.ContinueOnError).Returns(new BooleanToken(null, null, null, false));
|
||||
step.Setup(x => x.Stage).Returns(ActionRunStage.Main);
|
||||
step.Setup(x => x.Action)
|
||||
.Returns(new GitHub.DistributedTask.Pipelines.ActionStep()
|
||||
{
|
||||
Name = name,
|
||||
Id = stepRecordId,
|
||||
ContextName = contextName ?? name,
|
||||
});
|
||||
|
||||
var stepContext = new Mock<IExecutionContext>();
|
||||
stepContext.SetupAllProperties();
|
||||
stepContext.Setup(x => x.Global).Returns(() => _ec.Object.Global);
|
||||
stepContext.Setup(x => x.IsBackground).Returns(isBackground);
|
||||
var expressionValues = new DictionaryContextData();
|
||||
foreach (var pair in _ec.Object.ExpressionValues)
|
||||
{
|
||||
expressionValues[pair.Key] = pair.Value;
|
||||
}
|
||||
stepContext.Setup(x => x.ExpressionValues).Returns(expressionValues);
|
||||
stepContext.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
|
||||
stepContext.Setup(x => x.JobContext).Returns(_jobContext);
|
||||
stepContext.Setup(x => x.Id).Returns(stepRecordId);
|
||||
stepContext.Setup(x => x.ContextName).Returns(step.Object.Action.ContextName);
|
||||
stepContext.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||
stepContext.Setup(x => x.Complete(It.IsAny<TaskResult?>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((TaskResult? r, string currentOperation, string resultCode) =>
|
||||
{
|
||||
if (r != null)
|
||||
{
|
||||
stepContext.Object.Result = r;
|
||||
}
|
||||
_stepContext.SetOutcome("", stepContext.Object.ContextName, (stepContext.Object.Outcome ?? stepContext.Object.Result ?? TaskResult.Succeeded).ToActionResult());
|
||||
_stepContext.SetConclusion("", stepContext.Object.ContextName, (stepContext.Object.Result ?? TaskResult.Succeeded).ToActionResult());
|
||||
});
|
||||
stepContext.Setup(x => x.StepEnvironmentOverrides).Returns(new List<string>());
|
||||
stepContext.Setup(x => x.ApplyContinueOnError(It.IsAny<TemplateToken>()));
|
||||
stepContext.Setup(x => x.FlushDeferredOutputs()).Callback(() =>
|
||||
{
|
||||
if (stepContext.Object.DeferredOutputs != null)
|
||||
{
|
||||
foreach (var kvp in stepContext.Object.DeferredOutputs)
|
||||
{
|
||||
_stepContext.SetOutput("", stepContext.Object.ContextName, kvp.Key, kvp.Value, out _);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
step.Setup(x => x.RunAsync()).Returns(Task.CompletedTask);
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
private JobExtensionRunner CreateWaitStep(TestHostContext hc, string[] stepIds, Dictionary<string, string> timelineVariables = null)
|
||||
{
|
||||
var waitData = new BackgroundStepControlFlowData
|
||||
{
|
||||
Type = Pipelines.BackgroundControlTypes.Wait,
|
||||
StepIds = stepIds,
|
||||
};
|
||||
var bgCoordinator = hc.GetService<IBackgroundStepCoordinator>();
|
||||
var waitRunner = new JobExtensionRunner(
|
||||
runAsync: bgCoordinator.RunControlFlowAsync,
|
||||
condition: "success()",
|
||||
displayName: "Wait",
|
||||
data: waitData);
|
||||
|
||||
var stepContext = new Mock<IExecutionContext>();
|
||||
stepContext.SetupAllProperties();
|
||||
stepContext.Setup(x => x.Global).Returns(() => _ec.Object.Global);
|
||||
var waitExprValues = new DictionaryContextData();
|
||||
foreach (var pair in _ec.Object.ExpressionValues) { waitExprValues[pair.Key] = pair.Value; }
|
||||
stepContext.Setup(x => x.ExpressionValues).Returns(waitExprValues);
|
||||
stepContext.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
|
||||
stepContext.Setup(x => x.ContextName).Returns("__wait");
|
||||
stepContext.Setup(x => x.JobContext).Returns(_jobContext);
|
||||
stepContext.Setup(x => x.ScopeName).Returns((string)null);
|
||||
stepContext.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||
stepContext.Setup(x => x.StepEnvironmentOverrides).Returns(new List<string>());
|
||||
stepContext.Setup(x => x.Complete(It.IsAny<TaskResult?>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((TaskResult? r, string currentOperation, string resultCode) =>
|
||||
{
|
||||
if (r != null) 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}"); });
|
||||
|
||||
waitRunner.ExecutionContext = stepContext.Object;
|
||||
return waitRunner;
|
||||
}
|
||||
|
||||
private JobExtensionRunner CreateWaitAllStep(TestHostContext hc, Dictionary<string, string> timelineVariables = null)
|
||||
{
|
||||
var waitAllData = new BackgroundStepControlFlowData
|
||||
{
|
||||
Type = Pipelines.BackgroundControlTypes.WaitAll,
|
||||
};
|
||||
var bgCoordinator2 = hc.GetService<IBackgroundStepCoordinator>();
|
||||
var waitAllRunner = new JobExtensionRunner(
|
||||
runAsync: bgCoordinator2.RunControlFlowAsync,
|
||||
condition: "success()",
|
||||
displayName: "Wait All",
|
||||
data: waitAllData);
|
||||
|
||||
var stepContext = new Mock<IExecutionContext>();
|
||||
stepContext.SetupAllProperties();
|
||||
stepContext.Setup(x => x.Global).Returns(() => _ec.Object.Global);
|
||||
var waitAllExprValues = new DictionaryContextData();
|
||||
foreach (var pair in _ec.Object.ExpressionValues) { waitAllExprValues[pair.Key] = pair.Value; }
|
||||
stepContext.Setup(x => x.ExpressionValues).Returns(waitAllExprValues);
|
||||
stepContext.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
|
||||
stepContext.Setup(x => x.ContextName).Returns("__wait-all");
|
||||
stepContext.Setup(x => x.JobContext).Returns(_jobContext);
|
||||
stepContext.Setup(x => x.ScopeName).Returns((string)null);
|
||||
stepContext.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||
stepContext.Setup(x => x.StepEnvironmentOverrides).Returns(new List<string>());
|
||||
stepContext.Setup(x => x.Complete(It.IsAny<TaskResult?>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((TaskResult? r, string currentOperation, string resultCode) =>
|
||||
{
|
||||
if (r != null) 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}"); });
|
||||
|
||||
waitAllRunner.ExecutionContext = stepContext.Object;
|
||||
return waitAllRunner;
|
||||
}
|
||||
|
||||
private JobExtensionRunner CreateCancelStep(TestHostContext hc, string cancelStepId, Dictionary<string, string> timelineVariables = null)
|
||||
{
|
||||
var cancelData = new BackgroundStepControlFlowData
|
||||
{
|
||||
Type = Pipelines.BackgroundControlTypes.Cancel,
|
||||
StepIds = new[] { cancelStepId },
|
||||
};
|
||||
var bgCoordinator3 = hc.GetService<IBackgroundStepCoordinator>();
|
||||
var cancelRunner = new JobExtensionRunner(
|
||||
runAsync: bgCoordinator3.RunControlFlowAsync,
|
||||
condition: "success()",
|
||||
displayName: "Cancel",
|
||||
data: cancelData);
|
||||
|
||||
var stepContext = new Mock<IExecutionContext>();
|
||||
stepContext.SetupAllProperties();
|
||||
stepContext.Setup(x => x.Global).Returns(() => _ec.Object.Global);
|
||||
var cancelExprValues = new DictionaryContextData();
|
||||
foreach (var pair in _ec.Object.ExpressionValues) { cancelExprValues[pair.Key] = pair.Value; }
|
||||
stepContext.Setup(x => x.ExpressionValues).Returns(cancelExprValues);
|
||||
stepContext.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
|
||||
stepContext.Setup(x => x.ContextName).Returns("__cancel");
|
||||
stepContext.Setup(x => x.JobContext).Returns(_jobContext);
|
||||
stepContext.Setup(x => x.ScopeName).Returns((string)null);
|
||||
stepContext.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
|
||||
stepContext.Setup(x => x.StepEnvironmentOverrides).Returns(new List<string>());
|
||||
stepContext.Setup(x => x.Complete(It.IsAny<TaskResult?>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback((TaskResult? r, string currentOperation, string resultCode) =>
|
||||
{
|
||||
if (r != null) 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}"); });
|
||||
|
||||
cancelRunner.ExecutionContext = stepContext.Object;
|
||||
return cancelRunner;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -833,7 +833,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
Assert.True(response.Success);
|
||||
var body = Assert.IsType<SourceResponseBody>(response.Body);
|
||||
Assert.Equal(
|
||||
"pre:\n - step: \"Set up job\"\n - step: \"Pre cache\"\n\nmain:\n - step: \"Checkout\"\n - step: \"***\"\n\npost:\n - step: \"Post cache\"\n - step: \"Complete job\"\n",
|
||||
"pre:\n - step: \"Setup job\"\n - step: \"Pre cache\"\n\nmain:\n - step: \"Checkout\"\n - step: \"***\"\n\npost:\n - step: \"Post cache\"\n - step: \"Complete job\"\n",
|
||||
body.Content);
|
||||
Assert.Null(body.MimeType);
|
||||
|
||||
@@ -1011,7 +1011,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
new SourceArguments { SourceReference = 1 }));
|
||||
var sourceBody = Assert.IsType<SourceResponseBody>(sourceResponse.Body);
|
||||
Assert.Equal(
|
||||
"pre:\n - step: \"Set up job\"\n\nmain:\n - step: \"Checkout\"\n\npost:\n - step: \"Post Checkout\"\n - step: \"Complete job\"\n",
|
||||
"pre:\n - step: \"Setup job\"\n\nmain:\n - step: \"Checkout\"\n\npost:\n - step: \"Post Checkout\"\n - step: \"Complete job\"\n",
|
||||
sourceBody.Content);
|
||||
|
||||
var post = CreateActionRunner("Post Checkout", ActionRunStage.Post, action);
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
using System;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
public sealed class JobExecutionViewL0
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void RendersPreMainAndPostSections()
|
||||
{
|
||||
var pre = CreateStep("Pre cache", ActionRunStage.Pre);
|
||||
var checkout = CreateStep("Checkout");
|
||||
var post = CreateStep("Post cache", ActionRunStage.Post);
|
||||
|
||||
var view = new JobExecutionView(
|
||||
"job",
|
||||
new[] { pre.Object, checkout.Object },
|
||||
new[] { post.Object });
|
||||
|
||||
Assert.Equal(
|
||||
"pre:\n - step: \"Set up job\"\n - step: \"Pre cache\"\n\nmain:\n - step: \"Checkout\"\n\npost:\n - step: \"Post cache\"\n - step: \"Complete job\"\n",
|
||||
view.Content);
|
||||
Assert.Equal(3, view.TryGetLineForStep(pre.Object));
|
||||
Assert.Equal(6, view.TryGetLineForStep(checkout.Object));
|
||||
Assert.Equal(9, view.TryGetLineForStep(post.Object));
|
||||
Assert.Equal(10, view.CompleteJobLine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void ClaimsPredictedPostStepWithoutChangingLine()
|
||||
{
|
||||
var action = CreateRepositoryActionStep("actions/cache");
|
||||
var checkout = CreateActionRunner("Checkout", ActionRunStage.Main, action);
|
||||
var predicted = new JobExecutionView.PredictedPostStep(
|
||||
"Post Checkout",
|
||||
MatchKeyFor(action.Id));
|
||||
|
||||
var view = new JobExecutionView(
|
||||
"job",
|
||||
new[] { checkout.Object },
|
||||
Array.Empty<IStep>(),
|
||||
new[] { predicted });
|
||||
|
||||
var post = CreateActionRunner("Post Checkout", ActionRunStage.Post, action);
|
||||
var line = view.TryClaimPredictedStep(MatchKeyFor(action.Id), post.Object);
|
||||
|
||||
Assert.Equal(8, line);
|
||||
Assert.Equal(8, view.TryGetLineForStep(post.Object));
|
||||
Assert.Equal(
|
||||
"pre:\n - step: \"Set up job\"\n\nmain:\n - step: \"Checkout\"\n\npost:\n - step: \"Post Checkout\"\n - step: \"Complete job\"\n",
|
||||
view.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void UsesSyntheticCompleteJobLineWhenPostStepSharesName()
|
||||
{
|
||||
var checkout = CreateStep("Checkout");
|
||||
var realPost = CreateStep("Complete job", ActionRunStage.Post);
|
||||
|
||||
var view = new JobExecutionView(
|
||||
"job",
|
||||
new[] { checkout.Object },
|
||||
new[] { realPost.Object });
|
||||
|
||||
Assert.Equal(8, view.TryGetLineForStep(realPost.Object));
|
||||
Assert.Equal(9, view.CompleteJobLine);
|
||||
}
|
||||
|
||||
private static Mock<IStep> CreateStep(string displayName, ActionRunStage? stage = null)
|
||||
{
|
||||
var step = new Mock<IStep>();
|
||||
step.Setup(s => s.DisplayName).Returns(displayName);
|
||||
if (stage.HasValue)
|
||||
{
|
||||
var executionContext = new Mock<IExecutionContext>();
|
||||
executionContext.Setup(x => x.Stage).Returns(stage.Value);
|
||||
step.Setup(s => s.ExecutionContext).Returns(executionContext.Object);
|
||||
}
|
||||
else
|
||||
{
|
||||
step.Setup(s => s.ExecutionContext).Returns((IExecutionContext)null);
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
private static Mock<IActionRunner> CreateActionRunner(string displayName, ActionRunStage stage, ActionStep action)
|
||||
{
|
||||
var executionContext = new Mock<IExecutionContext>();
|
||||
executionContext.Setup(x => x.Stage).Returns(stage);
|
||||
|
||||
var runner = new Mock<IActionRunner>();
|
||||
runner.Setup(s => s.DisplayName).Returns(displayName);
|
||||
runner.Setup(s => s.ExecutionContext).Returns(executionContext.Object);
|
||||
runner.Setup(s => s.Stage).Returns(stage);
|
||||
runner.Setup(s => s.Action).Returns(action);
|
||||
return runner;
|
||||
}
|
||||
|
||||
private static ActionStep CreateRepositoryActionStep(string name)
|
||||
{
|
||||
return new ActionStep
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Reference = new RepositoryPathReference
|
||||
{
|
||||
Name = name,
|
||||
Ref = "v1",
|
||||
RepositoryType = RepositoryTypes.GitHub
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string MatchKeyFor(Guid actionId)
|
||||
{
|
||||
return $"post:{actionId:N}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,10 +549,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
var _stepsRunner = new StepsRunner();
|
||||
_stepsRunner.Initialize(hc);
|
||||
|
||||
var bgCoordinator = new BackgroundStepCoordinator();
|
||||
bgCoordinator.Initialize(hc);
|
||||
hc.SetSingleton<IBackgroundStepCoordinator>(bgCoordinator);
|
||||
|
||||
var mockDapDebugger = new Mock<IDapDebugger>();
|
||||
hc.SetSingleton(mockDapDebugger.Object);
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
_stepsRunner = new StepsRunner();
|
||||
_stepsRunner.Initialize(hc);
|
||||
|
||||
var bgCoordinator = new BackgroundStepCoordinator();
|
||||
bgCoordinator.Initialize(hc);
|
||||
hc.SetSingleton<IBackgroundStepCoordinator>(bgCoordinator);
|
||||
|
||||
var mockDapDebugger = new Mock<IDapDebugger>();
|
||||
hc.SetSingleton(mockDapDebugger.Object);
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.335.0
|
||||
2.334.0
|
||||
|
||||
Reference in New Issue
Block a user