Compare commits

..

3 Commits

Author SHA1 Message Date
Francesco Renzi
06ae0d86a3 Add DAP execution view coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-03 13:47:12 +01:00
Francesco Renzi
e615bd8a80 Add DAP source message coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-03 13:47:12 +01:00
Francesco Renzi
d8a18c194c Add DAP job execution view source
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-03 13:47:12 +01:00
49 changed files with 150 additions and 2843 deletions

View File

@@ -53,7 +53,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
# Build runner layout
- name: Build & Layout Release
@@ -95,7 +95,7 @@ jobs:
docker_platform: linux/arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Get latest runner version
id: latest_runner

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -29,7 +29,7 @@ jobs:
npm-vulnerabilities: ${{ steps.check-versions.outputs.npm-vulnerabilities }}
open-dependency-prs: ${{ steps.check-prs.outputs.open-dependency-prs }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:

View File

@@ -17,7 +17,7 @@ jobs:
BUILDX_CURRENT_VERSION: ${{ steps.check_buildx_version.outputs.CURRENT_VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Check Docker version
id: check_docker_version
@@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Update Docker version
shell: bash

View File

@@ -20,7 +20,7 @@ jobs:
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.releaseBranch }}

View File

@@ -15,7 +15,7 @@ jobs:
DOTNET_CURRENT_MAJOR_MINOR_VERSION: ${{ steps.fetch_current_version.outputs.DOTNET_CURRENT_MAJOR_MINOR_VERSION }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Get current major minor version
id: fetch_current_version
shell: bash
@@ -89,7 +89,7 @@ jobs:
if: ${{ needs.dotnet-update.outputs.SHOULD_UPDATE == 1 && needs.dotnet-update.outputs.BRANCH_EXISTS == 0 }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
ref: feature/dotnetsdk-upgrade/${{ needs.dotnet-update.outputs.DOTNET_LATEST_MAJOR_MINOR_PATCH_VERSION }}
- name: Create Pull Request

View File

@@ -9,7 +9,7 @@ jobs:
update-node:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Get latest Node versions
id: node-versions
run: |

View File

@@ -7,7 +7,7 @@ jobs:
npm-audit-with-ts-fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:

View File

@@ -9,7 +9,7 @@ jobs:
npm-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6

View File

@@ -11,7 +11,7 @@ jobs:
if: startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
# Make sure ./releaseVersion match ./src/runnerversion
# Query GitHub release ensure version is not used
@@ -86,7 +86,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
# Build runner layout
- name: Build & Layout Release
@@ -129,7 +129,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
# Download runner package tar.gz/zip produced by 'build' job
- name: Download Artifact (win-x64)
@@ -296,7 +296,7 @@ jobs:
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Compute image version
id: image

View File

@@ -5,8 +5,8 @@ ARG TARGETOS
ARG TARGETARCH
ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
ARG DOCKER_VERSION=29.6.1
ARG BUILDX_VERSION=0.35.0
ARG DOCKER_VERSION=29.5.0
ARG BUILDX_VERSION=0.34.0
RUN apt update -y && apt install curl unzip -y

View File

@@ -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.

View File

@@ -7,7 +7,7 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
NODE20_VERSION="20.20.2"
NODE24_VERSION="24.18.0"
NODE24_VERSION="24.16.0"
get_abs_path() {
# exploits the fact that pwd will print abs path when no args

View File

@@ -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;
}

View File

@@ -180,7 +180,6 @@ namespace GitHub.Runner.Common
public static readonly string BatchActionResolution = "actions_batch_action_resolution";
public static readonly string UseBearerTokenForCodeload = "actions_use_bearer_token_for_codeload";
public static readonly string OverrideDebuggerWelcomeMessage = "actions_runner_override_debugger_welcome_message";
public static readonly string SelfRepository = "actions_self_repository";
}
// Node version migration related constants
@@ -309,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";

View File

@@ -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
{

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -178,7 +178,7 @@ namespace GitHub.Runner.Worker
return new PrepareResult(containerSetupSteps, result.PreStepTracker);
}
private async Task<PrepareActionsState> PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Dictionary<string, WebApi.ActionDownloadInfo> resolvedDownloadInfos, Int32 depth = 0, Guid parentStepId = default(Guid), string selfRepoName = null, string selfRepoRef = null)
private async Task<PrepareActionsState> PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Dictionary<string, WebApi.ActionDownloadInfo> resolvedDownloadInfos, Int32 depth = 0, Guid parentStepId = default(Guid))
{
ArgUtil.NotNull(executionContext, nameof(executionContext));
if (depth > Constants.CompositeActionsMaxDepth)
@@ -186,21 +186,6 @@ namespace GitHub.Runner.Worker
throw new Exception($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}");
}
// Resolve self-repository ($/) references before processing
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
{
if (string.IsNullOrEmpty(selfRepoName))
{
// job.workflow_repository/workflow_sha point to the repo
// containing the workflow file — correct for both regular
// and reusable workflows. Always present when the server
// supports $/. See: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context
selfRepoName = executionContext.JobContext?.WorkflowRepository;
selfRepoRef = executionContext.JobContext?.WorkflowSha;
}
ResolveSelfRepositoryReferences(executionContext, actions, selfRepoName, selfRepoRef);
}
var repositoryActions = new List<Pipelines.ActionStep>();
foreach (var action in actions)
@@ -243,30 +228,7 @@ namespace GitHub.Runner.Worker
{
throw new Exception($"Missing download info for {lookupKey}");
}
Exception downloadFailure = null;
try
{
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
}
catch (Exception ex)
{
// record the exception for telemetry, and rethrow the original exception to fail the step.
downloadFailure = ex;
throw;
}
finally
{
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{
Type = JobTelemetryType.General,
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
{
Operation = "download_action",
Result = downloadFailure == null ? "succeeded" : downloadFailure.GetType().Name
}, Newtonsoft.Json.Formatting.None)}"
});
}
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
}
// Parse action.yml and collect composite sub-actions for batched
@@ -316,53 +278,16 @@ namespace GitHub.Runner.Worker
// then recurse per parent (which hits the cache, not the API).
if (nextLevel.Count > 0)
{
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
var nextLevelRepoActions = nextLevel
.Where(x => x.action.Reference.Type == Pipelines.ActionSourceType.Repository)
.Select(x => x.action)
.ToList();
await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos);
foreach (var group in nextLevel.GroupBy(x => x.parentId))
{
// Self-repository path: group by parent so each group's
// $/ refs resolve against the correct parent repo context.
var groups = nextLevel.GroupBy(x => x.parentId).Select(group =>
{
string childRepoName = selfRepoName;
string childRepoRef = selfRepoRef;
var parentAction = repositoryActions.FirstOrDefault(a => a.Id == group.Key);
if (parentAction?.Reference is Pipelines.RepositoryPathReference parentRef &&
string.Equals(parentRef.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
{
childRepoName = parentRef.Name;
childRepoRef = parentRef.Ref;
}
return new { ParentId = group.Key, Actions = group.Select(x => x.action).ToList(), RepoName = childRepoName, RepoRef = childRepoRef };
}).ToList();
foreach (var group in groups)
{
ResolveSelfRepositoryReferences(executionContext, group.Actions, group.RepoName, group.RepoRef);
}
var nextLevelRepoActions = nextLevel
.Where(x => x.action.Reference.Type == Pipelines.ActionSourceType.Repository)
.Select(x => x.action)
.ToList();
await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos);
foreach (var group in groups)
{
state = await PrepareActionsRecursiveAsync(executionContext, state, group.Actions, resolvedDownloadInfos, depth + 1, group.ParentId, group.RepoName, group.RepoRef);
}
}
else
{
// Original path: no self-repository resolution needed.
var nextLevelActions = nextLevel.Select(x => x.action).ToList();
var nextLevelRepoActions = nextLevelActions
.Where(x => x.Reference.Type == Pipelines.ActionSourceType.Repository)
.ToList();
await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos);
foreach (var grp in nextLevel.GroupBy(x => x.parentId))
{
state = await PrepareActionsRecursiveAsync(executionContext, state, grp.Select(x => x.action).ToList(), resolvedDownloadInfos, depth + 1, grp.Key);
}
var groupActions = group.Select(x => x.action).ToList();
state = await PrepareActionsRecursiveAsync(executionContext, state, groupActions, resolvedDownloadInfos, depth + 1, group.Key);
}
}
@@ -438,25 +363,13 @@ namespace GitHub.Runner.Worker
/// sub-actions individually, with no cross-depth deduplication.
/// Used when the BatchActionResolution feature flag is disabled.
/// </summary>
private async Task<PrepareActionsState> PrepareActionsRecursiveLegacyAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Int32 depth = 0, Guid parentStepId = default(Guid), string selfRepoName = null, string selfRepoRef = null)
private async Task<PrepareActionsState> PrepareActionsRecursiveLegacyAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable<Pipelines.ActionStep> actions, Int32 depth = 0, Guid parentStepId = default(Guid))
{
ArgUtil.NotNull(executionContext, nameof(executionContext));
if (depth > Constants.CompositeActionsMaxDepth)
{
throw new Exception($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}");
}
// Resolve self-repository ($/) references before processing
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
{
if (string.IsNullOrEmpty(selfRepoName))
{
selfRepoName = executionContext.JobContext?.WorkflowRepository;
selfRepoRef = executionContext.JobContext?.WorkflowSha;
}
ResolveSelfRepositoryReferences(executionContext, actions, selfRepoName, selfRepoRef);
}
var repositoryActions = new List<Pipelines.ActionStep>();
foreach (var action in actions)
@@ -485,30 +398,7 @@ namespace GitHub.Runner.Worker
if (repositoryActions.Count > 0)
{
// Get the download info
IDictionary<string, WebApi.ActionDownloadInfo> downloadInfos = null;
Exception resolveFailure = null;
try
{
downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions);
}
catch (Exception ex)
{
// record the exception for telemetry, and rethrow the original exception to fail the step.
resolveFailure = ex;
throw;
}
finally
{
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{
Type = JobTelemetryType.General,
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
{
Operation = "resolve_actions",
Result = resolveFailure == null ? "succeeded" : resolveFailure.GetType().Name
}, Newtonsoft.Json.Formatting.None)}"
});
}
var downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions);
// Download each action
foreach (var action in repositoryActions)
@@ -524,29 +414,7 @@ namespace GitHub.Runner.Worker
throw new Exception($"Missing download info for {lookupKey}");
}
Exception downloadFailure = null;
try
{
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
}
catch (Exception ex)
{
// record the exception for telemetry, and rethrow the original exception to fail the step.
downloadFailure = ex;
throw;
}
finally
{
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{
Type = JobTelemetryType.General,
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
{
Operation = "download_action",
Result = downloadFailure == null ? "succeeded" : downloadFailure.GetType().Name
}, Newtonsoft.Json.Formatting.None)}"
});
}
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
}
// More preparation based on content in the repository (action.yml)
@@ -581,17 +449,7 @@ namespace GitHub.Runner.Worker
}
else if (setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0)
{
// Propagate parent's repo context for nested self-repository resolution
var parentRef = action.Reference as Pipelines.RepositoryPathReference;
var childRepoName = selfRepoName;
var childRepoRef = selfRepoRef;
if (parentRef != null &&
string.Equals(parentRef.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
{
childRepoName = parentRef.Name;
childRepoRef = parentRef.Ref;
}
state = await PrepareActionsRecursiveLegacyAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id, childRepoName, childRepoRef);
state = await PrepareActionsRecursiveLegacyAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id);
}
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias)
@@ -704,12 +562,6 @@ namespace GitHub.Runner.Worker
actionDirectory = Path.Combine(actionDirectory, repoAction.Path);
}
}
else if (string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
{
// Unresolved self-repository reference at load time — this
// shouldn't happen but guard against NRE if it does.
throw new InvalidOperationException($"Self-repository reference '$/{repoAction.Path}' was not resolved before LoadAction. Ensure the '{Constants.Runner.Features.SelfRepository}' feature flag is enabled.");
}
else
{
actionDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), repoAction.Name.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), repoAction.Ref);
@@ -845,27 +697,6 @@ namespace GitHub.Runner.Worker
_cachedEmbeddedStepIds[action.Id].Add(guid);
}
}
// Resolve self-repository refs in composite steps at load time.
// During setup, resolution happens on a separate copy of these
// step objects. At runtime, action.yml is re-parsed, producing
// fresh self-repository refs that need resolution here.
// When the parent is a dot-slash (self local-workspace) action,
// repoAction.Name/Ref are null — fall back to workflow context.
if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SelfRepository) == true)
{
var parentName = repoAction.Name ?? executionContext.JobContext?.WorkflowRepository;
var parentRef = repoAction.Ref ?? executionContext.JobContext?.WorkflowSha;
ResolveSelfRepositoryReferences(executionContext, compositeAction.Steps, parentName, parentRef);
if (compositeAction.PreSteps != null)
{
ResolveSelfRepositoryReferences(executionContext, compositeAction.PreSteps, parentName, parentRef);
}
if (compositeAction.PostSteps != null)
{
ResolveSelfRepositoryReferences(executionContext, compositeAction.PostSteps, parentName, parentRef);
}
}
}
else
{
@@ -1149,33 +980,10 @@ namespace GitHub.Runner.Worker
if (actionsToResolve.Count > 0)
{
IDictionary<string, WebApi.ActionDownloadInfo> downloadInfos = null;
Exception resolveFailure = null;
try
var downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve);
foreach (var kvp in downloadInfos)
{
downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve);
foreach (var kvp in downloadInfos)
{
resolvedDownloadInfos[kvp.Key] = kvp.Value;
}
}
catch (Exception ex)
{
// record the exception for telemetry, and rethrow the original exception to fail the step.
resolveFailure = ex;
throw;
}
finally
{
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{
Type = JobTelemetryType.General,
Message = $"resolve_download_actions_telemetry:{StringUtil.ConvertToJson(new ActionTelemetryPayload
{
Operation = "resolve_actions",
Result = resolveFailure == null ? "succeeded" : resolveFailure.GetType().Name
}, Newtonsoft.Json.Formatting.None)}"
});
resolvedDownloadInfos[kvp.Key] = kvp.Value;
}
}
}
@@ -1300,6 +1108,12 @@ namespace GitHub.Runner.Worker
}
}
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{
Type = JobTelemetryType.General,
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache}"
});
if (!useActionArchiveCache)
{
await DownloadRepositoryArchive(executionContext, link, downloadInfo.Authentication?.Token, archiveFile);
@@ -1308,13 +1122,6 @@ namespace GitHub.Runner.Worker
var stagingDirectory = Path.Combine(tempDirectory, "_staging");
Directory.CreateDirectory(stagingDirectory);
var fileInfo = new FileInfo(archiveFile);
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{
Type = JobTelemetryType.General,
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache} size {fileInfo.Length} bytes"
});
#if OS_WINDOWS
try
{
@@ -1352,6 +1159,7 @@ namespace GitHub.Runner.Worker
int exitCode = await processInvoker.ExecuteAsync(stagingDirectory, tar, $"-xzf \"{archiveFile}\"", null, executionContext.CancellationToken);
if (exitCode != 0)
{
var fileInfo = new FileInfo(archiveFile);
var sha256hash = await IOUtil.GetFileContentSha256HashAsync(archiveFile);
throw new InvalidActionArchiveException($"Can't use 'tar -xzf' extract archive file: {archiveFile} (SHA256 '{sha256hash}', size '{fileInfo.Length}' bytes, tar outputs '{string.Join(' ', tarOutputs)}'). Action being checked out: {downloadInfo.NameWithOwner}@{downloadInfo.Ref}. return code: {exitCode}.");
}
@@ -1401,12 +1209,6 @@ namespace GitHub.Runner.Worker
private string GetWatermarkFilePath(string directory) => directory + ".completed";
private sealed class ActionTelemetryPayload
{
public string Operation { get; set; }
public string Result { get; set; }
}
private ActionSetupInfo PrepareRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction)
{
var repositoryReference = repositoryAction.Reference as Pipelines.RepositoryPathReference;
@@ -1545,47 +1347,6 @@ namespace GitHub.Runner.Worker
}
}
/// <summary>
/// Resolves self-reference ($/) references by mutating them in-place
/// to standard GitHub repository references with the containing repo's
/// name and ref.
/// </summary>
private void ResolveSelfRepositoryReferences(IExecutionContext executionContext, IEnumerable<Pipelines.ActionStep> actions, string repoName, string repoRef)
{
if (string.IsNullOrEmpty(repoName) || string.IsNullOrEmpty(repoRef))
{
return;
}
foreach (var action in actions)
{
if (action.Reference.Type != Pipelines.ActionSourceType.Repository)
{
continue;
}
var repoAction = action.Reference as Pipelines.RepositoryPathReference;
if (!string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
{
continue;
}
Trace.Info($"Resolving self-repository reference reference '$/{repoAction.Path}' to '{repoName}/{repoAction.Path}@{repoRef}'");
executionContext.Debug($"Resolving $/{repoAction.Path} → {repoName}/{repoAction.Path}@{repoRef}");
repoAction.RepositoryType = Pipelines.RepositoryTypes.GitHub;
repoAction.Name = repoName;
repoAction.Ref = repoRef;
}
}
/// <summary>
/// If this is a reusable workflow job, ensure the workflow repo tarball
/// is downloaded so self.workspace resolves to a real path on disk.
/// Always downloads for reusable workflows when the feature flag is on,
/// since step expressions are already expanded by the server and can't
/// be scanned for self.* usage.
/// </summary>
private static string GetDownloadInfoLookupKey(Pipelines.ActionStep action)
{
if (action.Reference.Type != Pipelines.ActionSourceType.Repository)
@@ -1601,11 +1362,6 @@ namespace GitHub.Runner.Worker
return null;
}
if (string.Equals(repositoryReference.RepositoryType, Pipelines.PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Unable to resolve self-reference '$/'. This can occur when the server does not support this syntax, the feature flag is disabled, or the workflow context (repository/SHA) is unavailable.");
}
if (!string.Equals(repositoryReference.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase))
{
throw new NotSupportedException(repositoryReference.RepositoryType);

View File

@@ -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; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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}");
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -55,18 +55,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
break;
case ActionSourceType.Repository:
var repositoryReference = step.Reference as RepositoryPathReference;
if (!String.IsNullOrEmpty(repositoryReference.Name))
{
name = repositoryReference.Name;
}
else if (String.Equals(repositoryReference.RepositoryType, PipelineConstants.SelfRepositoryAlias, StringComparison.OrdinalIgnoreCase))
{
name = PipelineConstants.SelfRepositoryAlias;
}
else
{
name = PipelineConstants.SelfAlias;
}
name = !String.IsNullOrEmpty(repositoryReference.Name) ? repositoryReference.Name : PipelineConstants.SelfAlias;
break;
}
@@ -611,14 +600,6 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
Path = uses.Value
};
}
else if (PipelineConstants.TryParseSelfRepository(uses.Value, out var selfPath))
{
result.Reference = new RepositoryPathReference
{
RepositoryType = PipelineConstants.SelfRepositoryAlias,
Path = selfPath
};
}
else
{
var usesSegments = uses.Value.Split('@');

View File

@@ -38,43 +38,10 @@ namespace GitHub.DistributedTask.Pipelines
public static readonly Int32 MaxNodeNameLength = 100;
/// <summary>
/// Alias for the self local-workspace repository type (./ syntax).
/// Resolves to the local checkout on the runner.
/// Alias for the self repository.
/// </summary>
public static readonly String SelfAlias = "self";
/// <summary>
/// RepositoryType for self-repository references ($/ syntax).
/// Resolves to "this repo, at this SHA" based on the containing YAML file.
/// </summary>
public static readonly String SelfRepositoryAlias = "selfRepository";
/// <summary>
/// The prefix for self-repository references in uses: values.
/// </summary>
public const String SelfRepositoryPrefix = "$/";
/// <summary>
/// Returns true if the uses value is a self-repository reference (starts with $/),
/// and outputs the subpath after the prefix.
/// </summary>
public static bool TryParseSelfRepository(string usesValue, out string path)
{
if (usesValue != null && usesValue.StartsWith(SelfRepositoryPrefix, StringComparison.Ordinal))
{
path = usesValue.Substring(SelfRepositoryPrefix.Length).TrimStart('/');
if (string.IsNullOrEmpty(path))
{
path = null;
return false;
}
return true;
}
path = null;
return false;
}
/// <summary>
/// Error code during graph validation.
/// </summary>

View File

@@ -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,
}
}

View File

@@ -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())

View File

@@ -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

View File

@@ -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; }
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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)

View File

@@ -1605,10 +1605,6 @@ namespace GitHub.Actions.WorkflowParser.Conversion
{
id = WorkflowConstants.SelfAlias;
}
else if (GitHub.DistributedTask.Pipelines.PipelineConstants.TryParseSelfRepository(action.Uses!.Value, out _))
{
id = WorkflowConstants.SelfRepositoryAlias;
}
else
{
var usesSegments = action.Uses!.Value.Split('@');

View File

@@ -26,15 +26,10 @@ namespace GitHub.Actions.WorkflowParser
internal const Int32 MaxNodeNameLength = 100;
/// <summary>
/// Alias for the self local-workspace repository type (./ syntax).
/// Alias for the self repository.
/// </summary>
internal const String SelfAlias = "self";
/// <summary>
/// RepositoryType for self-repository references ($/ syntax).
/// </summary>
internal const String SelfRepositoryAlias = "selfRepository";
public static class PermissionsPolicy
{
public const string LimitedRead = "LimitedRead";

View File

@@ -90,11 +90,6 @@ namespace GitHub.Runner.Common.Tests.Worker
var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "main", "action.yml");
Assert.True(File.Exists(actionYamlFile));
var telemetryMessages = GetTelemetryMessages();
Assert.True(ContainsTelemetry(telemetryMessages, "resolve_actions"));
Assert.True(ContainsTelemetry(telemetryMessages, "succeeded"));
Assert.True(ContainsTelemetry(telemetryMessages, "download_action"));
_hc.GetTrace().Info(File.ReadAllText(actionYamlFile));
}
finally
@@ -153,11 +148,6 @@ namespace GitHub.Runner.Common.Tests.Worker
// Act + Assert
await Assert.ThrowsAsync<InvalidActionArchiveException>(async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions));
var telemetryMessages = GetTelemetryMessages();
Assert.True(ContainsTelemetry(telemetryMessages, "resolve_actions"));
Assert.True(ContainsTelemetry(telemetryMessages, "download_action"));
Assert.True(ContainsTelemetry(telemetryMessages, "InvalidActionArchiveException"));
}
finally
{
@@ -225,51 +215,6 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task PrepareActions_ResolveActionDownloadInfo_RecordsTelemetry_OnFailure()
{
try
{
// Arrange
Setup();
_ec.Object.Global.Variables.Set(Constants.Variables.System.JobRequestType, "RunnerJobRequest");
_launchServer
.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.ThrowsAsync(new Exception("resolve failed"));
var actions = new List<Pipelines.JobStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/checkout",
Ref = "v4",
RepositoryType = "GitHub"
}
}
};
// Act + Assert
await Assert.ThrowsAsync<FailedToResolveActionDownloadInfoException>(async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions));
var telemetryMessages = GetTelemetryMessages();
Assert.Equal(1, telemetryMessages.Count(message =>
message.Contains("resolve_actions", StringComparison.OrdinalIgnoreCase)
&& !message.Contains("\"result\":\"succeeded\"", StringComparison.OrdinalIgnoreCase)));
Assert.False(ContainsTelemetry(telemetryMessages, "resolve_actions\",\"result\":\"succeeded"));
}
finally
{
Teardown();
}
}
#if OS_LINUX
[Fact]
[Trait("Level", "L0")]
@@ -585,9 +530,9 @@ runs:
//Assert
string destDirectory = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions", "checkout", "master");
Assert.True(Directory.Exists(destDirectory), "Destination directory does not exist");
var di = new DirectoryInfo(destDirectory);
Assert.NotNull(di.LinkTarget);
Assert.True(Directory.Exists(destDirectory), "Destination directory does not exist");
var di = new DirectoryInfo(destDirectory);
Assert.NotNull(di.LinkTarget);
}
finally
{
@@ -2441,7 +2386,7 @@ runs:
}
}
[Fact]
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void LoadsNode24ActionDefinition()
@@ -2509,7 +2454,7 @@ runs:
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -3388,16 +3333,6 @@ runs:
}
}
private IList<string> GetTelemetryMessages()
{
return _ec.Object.Global.JobTelemetry.Select(x => x.Message).ToList();
}
private static bool ContainsTelemetry(IList<string> telemetryMessages, string expectedFragment)
{
return telemetryMessages.Any(message => message.Contains(expectedFragment, StringComparison.OrdinalIgnoreCase));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -3533,604 +3468,5 @@ runs:
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SelfRepository_ResolvesAtDepthZero()
{
// Self-references are only supported via run service (batch resolution path)
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
try
{
// Arrange
Setup();
const string RepoName = "my-org/my-repo";
const string RepoSha = "abc123def456";
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
var jobContext = new JobContext();
jobContext.WorkflowRepository = RepoName;
jobContext.WorkflowSha = RepoSha;
_ec.Setup(x => x.JobContext).Returns(jobContext);
var actionId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
Path = "actions/my-action"
}
}
};
string archiveFile = await CreateRepoArchive();
using var stream = File.OpenRead(archiveFile);
string archiveLink = GetLinkToActionArchive("https://api.github.com", RepoName, RepoSha);
var mockClientHandler = new Mock<HttpClientHandler>();
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri == new Uri(archiveLink)), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) });
var mockHandlerFactory = new Mock<IHttpClientHandlerFactory>();
mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny<RunnerWebProxy>())).Returns(mockClientHandler.Object);
_hc.SetSingleton(mockHandlerFactory.Object);
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
// Act — resolution mutates the reference in-place before download/prepare.
// The archive doesn't contain the subpath, so prepare will fail, but the
// reference is already resolved by that point.
try
{
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("Can't find"))
{
// Expected: test archive lacks the action.yml at the resolved subpath
}
// Assert — the reference should be resolved to a GitHub repo reference
var repoRef = actions[0].Reference as Pipelines.RepositoryPathReference;
Assert.Equal(Pipelines.RepositoryTypes.GitHub, repoRef.RepositoryType);
Assert.Equal(RepoName, repoRef.Name);
Assert.Equal(RepoSha, repoRef.Ref);
Assert.Equal("actions/my-action", repoRef.Path);
}
finally
{
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SelfRepository_NotResolvedWhenFeatureFlagDisabled()
{
try
{
// Arrange
Setup();
_ec.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/my-repo");
_ec.Setup(x => x.GetGitHubContext("sha")).Returns("abc123");
// Feature flag NOT set
var actionId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
Path = "actions/my-action"
}
}
};
// Act & Assert — should throw because unresolved self-reference hits GetDownloadInfoLookupKey
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _actionManager.PrepareActionsAsync(_ec.Object, actions));
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SelfRepository_ResolvesNestedInComposite()
{
// Composite action at $/actions/parent uses $/actions/child (same repo).
// This tests the batch path fix: $/ refs in nextLevel must be resolved
// BEFORE ResolveNewActionsAsync, otherwise GetDownloadInfoLookupKey throws.
// We pre-stage only the parent action.yml on disk so the composite steps
// are discovered, but we DON'T stage the child — a download failure for
// the child is fine; the important thing is that $/ was resolved
// (no InvalidOperationException from GetDownloadInfoLookupKey).
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
try
{
// Arrange
Setup();
const string RepoName = "my-org/my-repo";
const string RepoSha = "abc123def456";
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
var jobContext = new JobContext();
jobContext.WorkflowRepository = RepoName;
jobContext.WorkflowSha = RepoSha;
_ec.Setup(x => x.JobContext).Returns(jobContext);
// Stage parent action on disk as a composite that uses $/actions/child.
// We use rootStepId != default to avoid directory deletion,
// and create the watermark + action.yml in the expected location.
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
string destDir = Path.Combine(actionsDir, RepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), RepoSha);
Directory.CreateDirectory(Path.Combine(destDir, "actions", "parent"));
File.WriteAllText(Path.Combine(destDir, "actions", "parent", Constants.Path.ActionManifestYmlFile), @"
name: 'Parent'
description: 'Composite parent'
runs:
using: 'composite'
steps:
- uses: $/actions/child
");
// Stage child action too (as a leaf node action)
Directory.CreateDirectory(Path.Combine(destDir, "actions", "child"));
File.WriteAllText(Path.Combine(destDir, "actions", "child", Constants.Path.ActionManifestYmlFile), @"
name: 'Child'
description: 'Node child'
runs:
using: 'node20'
main: 'index.js'
");
// Write watermark
File.WriteAllText($"{destDir}.completed", string.Empty);
var rootStepId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
Path = "actions/parent"
}
}
};
// Act — should resolve $/ and not throw InvalidOperationException
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
// Assert — top-level $/ resolved
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
Assert.Equal(Pipelines.RepositoryTypes.GitHub, topRef.RepositoryType);
Assert.Equal(RepoName, topRef.Name);
Assert.Equal(RepoSha, topRef.Ref);
Assert.Equal("actions/parent", topRef.Path);
}
finally
{
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SelfRepository_CrossRepoCompositeResolvesToParentRepo()
{
// External composite (external/foo@v1) uses $/lib/bar.
// $/lib/bar should resolve to external/foo@v1 (the parent's repo),
// NOT to the workflow's root repo.
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
try
{
// Arrange
Setup();
const string RootRepoName = "my-org/my-repo";
const string RootRepoSha = "root-sha-111";
const string ExtRepoName = "external/foo";
const string ExtRepoRef = "v1";
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RootRepoName);
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RootRepoSha);
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
var jobContext = new JobContext();
jobContext.WorkflowRepository = RootRepoName;
jobContext.WorkflowSha = RootRepoSha;
_ec.Setup(x => x.JobContext).Returns(jobContext);
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
string destDir = Path.Combine(actionsDir, ExtRepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), ExtRepoRef);
Directory.CreateDirectory(destDir);
File.WriteAllText(Path.Combine(destDir, Constants.Path.ActionManifestYmlFile), @"
name: 'External Foo'
description: 'External composite'
runs:
using: 'composite'
steps:
- uses: $/lib/bar
");
Directory.CreateDirectory(Path.Combine(destDir, "lib", "bar"));
File.WriteAllText(Path.Combine(destDir, "lib", "bar", Constants.Path.ActionManifestYmlFile), @"
name: 'Bar'
description: 'Node action in external repo'
runs:
using: 'node20'
main: 'index.js'
");
File.WriteAllText($"{destDir}.completed", string.Empty);
var rootStepId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = ExtRepoName,
Ref = ExtRepoRef,
RepositoryType = Pipelines.RepositoryTypes.GitHub
}
}
};
// Act — should resolve $/lib/bar to external/foo@v1/lib/bar
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
// Assert — the top-level ref is unchanged (it was already concrete)
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
Assert.Equal(ExtRepoName, topRef.Name);
Assert.Equal(ExtRepoRef, topRef.Ref);
}
finally
{
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SelfRepository_MultiLevelChain()
{
// $/a → composite → $/b → composite → $/c (three levels, same repo)
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true");
try
{
// Arrange
Setup();
const string RepoName = "my-org/my-repo";
const string RepoSha = "chain-sha-222";
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
var jobContext = new JobContext();
jobContext.WorkflowRepository = RepoName;
jobContext.WorkflowSha = RepoSha;
_ec.Setup(x => x.JobContext).Returns(jobContext);
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
string destDir = Path.Combine(actionsDir, RepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), RepoSha);
Directory.CreateDirectory(Path.Combine(destDir, "a"));
File.WriteAllText(Path.Combine(destDir, "a", Constants.Path.ActionManifestYmlFile), @"
name: 'A'
description: 'Level 0 composite'
runs:
using: 'composite'
steps:
- uses: $/b
");
Directory.CreateDirectory(Path.Combine(destDir, "b"));
File.WriteAllText(Path.Combine(destDir, "b", Constants.Path.ActionManifestYmlFile), @"
name: 'B'
description: 'Level 1 composite'
runs:
using: 'composite'
steps:
- uses: $/c
");
Directory.CreateDirectory(Path.Combine(destDir, "c"));
File.WriteAllText(Path.Combine(destDir, "c", Constants.Path.ActionManifestYmlFile), @"
name: 'C'
description: 'Level 2 node leaf'
runs:
using: 'node20'
main: 'index.js'
");
File.WriteAllText($"{destDir}.completed", string.Empty);
var rootStepId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
Path = "a"
}
}
};
// Act — three-level $/ chain should resolve without error
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
// Assert — top-level ref resolved
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
Assert.Equal(Pipelines.RepositoryTypes.GitHub, topRef.RepositoryType);
Assert.Equal(RepoName, topRef.Name);
Assert.Equal(RepoSha, topRef.Ref);
Assert.Equal("a", topRef.Path);
}
finally
{
Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null);
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SelfRepository_ResolvesAtDepthZero_LegacyPath()
{
// Same as ResolvesAtDepthZero but on the legacy (non-batch) path
try
{
// Arrange
Setup();
const string RepoName = "my-org/my-repo";
const string RepoSha = "abc123def456";
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
var jobContext = new JobContext();
jobContext.WorkflowRepository = RepoName;
jobContext.WorkflowSha = RepoSha;
_ec.Setup(x => x.JobContext).Returns(jobContext);
var actionId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
Path = "actions/my-action"
}
}
};
string archiveFile = await CreateRepoArchive();
using var stream = File.OpenRead(archiveFile);
string archiveLink = GetLinkToActionArchive("https://api.github.com", RepoName, RepoSha);
var mockClientHandler = new Mock<HttpClientHandler>();
mockClientHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(m => m.RequestUri == new Uri(archiveLink)), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) });
var mockHandlerFactory = new Mock<IHttpClientHandlerFactory>();
mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny<RunnerWebProxy>())).Returns(mockClientHandler.Object);
_hc.SetSingleton(mockHandlerFactory.Object);
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
// Act
try
{
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("Can't find"))
{
// Expected: test archive lacks the action.yml at the resolved subpath
}
// Assert — the reference should be resolved to a GitHub repo reference
var repoRef = actions[0].Reference as Pipelines.RepositoryPathReference;
Assert.Equal(Pipelines.RepositoryTypes.GitHub, repoRef.RepositoryType);
Assert.Equal(RepoName, repoRef.Name);
Assert.Equal(RepoSha, repoRef.Ref);
Assert.Equal("actions/my-action", repoRef.Path);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SelfRepository_ResolvesNestedInComposite_LegacyPath()
{
// Same as ResolvesNestedInComposite but on the legacy (non-batch) path.
// Verifies that $/ resolution works when batch action resolution is disabled.
try
{
// Arrange
Setup();
const string RepoName = "my-org/my-repo";
const string RepoSha = "abc123def456";
_ec.Setup(x => x.GetGitHubContext("repository")).Returns(RepoName);
_ec.Setup(x => x.GetGitHubContext("sha")).Returns(RepoSha);
_ec.Setup(x => x.GetGitHubContext("api_url")).Returns("https://api.github.com");
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
var jobContext = new JobContext();
jobContext.WorkflowRepository = RepoName;
jobContext.WorkflowSha = RepoSha;
_ec.Setup(x => x.JobContext).Returns(jobContext);
// Stage parent action on disk as a composite that uses $/actions/child.
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
string destDir = Path.Combine(actionsDir, RepoName.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), RepoSha);
Directory.CreateDirectory(Path.Combine(destDir, "actions", "parent"));
File.WriteAllText(Path.Combine(destDir, "actions", "parent", Constants.Path.ActionManifestYmlFile), @"
name: 'Parent'
description: 'Composite parent'
runs:
using: 'composite'
steps:
- uses: $/actions/child
");
// Stage child action too (as a leaf node action)
Directory.CreateDirectory(Path.Combine(destDir, "actions", "child"));
File.WriteAllText(Path.Combine(destDir, "actions", "child", Constants.Path.ActionManifestYmlFile), @"
name: 'Child'
description: 'Node child'
runs:
using: 'node20'
main: 'index.js'
");
// Write watermark
File.WriteAllText($"{destDir}.completed", string.Empty);
var rootStepId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfRepositoryAlias,
Path = "actions/parent"
}
}
};
// Act — should resolve $/ and not throw InvalidOperationException
await _actionManager.PrepareActionsAsync(_ec.Object, actions, rootStepId);
// Assert — top-level $/ resolved
var topRef = actions[0].Reference as Pipelines.RepositoryPathReference;
Assert.Equal(Pipelines.RepositoryTypes.GitHub, topRef.RepositoryType);
Assert.Equal(RepoName, topRef.Name);
Assert.Equal(RepoSha, topRef.Ref);
Assert.Equal("actions/parent", topRef.Path);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void LoadAction_DotSlashCompositeWithNestedSelfRepository_ResolvesViaWorkflowContext()
{
// Regression test: when a dot-slash (./) composite action contains a
// nested $/actions/child step, LoadAction re-parses the action.yml at
// runtime and must resolve the $/ ref. The parent is repositoryType "self"
// so its Name and Ref are null — resolution must fall back to
// WorkflowRepository/WorkflowSha from the job context. Before the fix,
// this path hit a NullReferenceException at repoAction.Name.Replace().
try
{
// Arrange
Setup();
const string WorkflowRepo = "my-org/my-repo";
const string WorkflowSha = "abc123def456";
_ec.Object.Global.Variables.Set(Constants.Runner.Features.SelfRepository, "true");
var jobContext = new JobContext();
jobContext.WorkflowRepository = WorkflowRepo;
jobContext.WorkflowSha = WorkflowSha;
_ec.Setup(x => x.JobContext).Returns(jobContext);
// Stage the dot-slash composite in the workspace directory.
// It contains a nested $/actions/child step.
string workspaceDir = Path.Combine(_workFolder, "actions", "actions");
string compositeDir = Path.Combine(workspaceDir, "my-composite");
Directory.CreateDirectory(compositeDir);
File.WriteAllText(Path.Combine(compositeDir, Constants.Path.ActionManifestYmlFile), @"
name: 'DotSlash Parent'
description: 'Composite loaded via ./ that nests a $/ ref'
runs:
using: 'composite'
steps:
- run: echo 'hello'
shell: bash
- uses: $/actions/child
");
// Stage the child action in the actions cache under the workflow repo.
string actionsDir = Path.Combine(_workFolder, Constants.Path.ActionsDirectory);
string childDir = Path.Combine(actionsDir, WorkflowRepo.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), WorkflowSha, "actions", "child");
Directory.CreateDirectory(childDir);
File.WriteAllText(Path.Combine(childDir, Constants.Path.ActionManifestYmlFile), @"
name: 'Child'
description: 'Leaf action'
runs:
using: 'node20'
main: 'index.js'
");
// Create dot-slash step with Name = null (the real scenario).
var instance = new Pipelines.ActionStep()
{
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = null,
Ref = null,
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
Path = "my-composite"
}
};
// Act — should NOT throw NullReferenceException
Definition definition = _actionManager.LoadAction(_ec.Object, instance);
// Assert — loaded the composite successfully
Assert.NotNull(definition);
Assert.NotNull(definition.Data);
Assert.Equal(ActionExecutionType.Composite, definition.Data.Execution.ExecutionType);
// Assert — the nested $/ step was resolved to the workflow repo
var compositeData = definition.Data.Execution as CompositeActionExecutionData;
Assert.NotNull(compositeData);
var childStep = compositeData.Steps
.OfType<Pipelines.ActionStep>()
.FirstOrDefault(s => s.Reference is Pipelines.RepositoryPathReference r
&& r.Path == "actions/child");
Assert.NotNull(childStep);
var childRef = childStep.Reference as Pipelines.RepositoryPathReference;
Assert.Equal(Pipelines.RepositoryTypes.GitHub, childRef.RepositoryType);
Assert.Equal(WorkflowRepo, childRef.Name);
Assert.Equal(WorkflowSha, childRef.Ref);
}
finally
{
Teardown();
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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}";
}
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -1 +1 @@
2.335.0
2.334.0