Compare commits

...

11 Commits

Author SHA1 Message Date
Philip Gai
49a88c1161 test: address review feedback for ACTIONS_CACHE_MODE tests
- Add ContainerActionHandler L0 coverage (Linux-gated) asserting ACTIONS_CACHE_MODE
  is exported to the container env when actions_cache_mode is set and absent
  otherwise, routed through the container-hooks path.
- Set the cache-mode variable directly on the initialized job context instead of
  re-invoking InitializeJob, avoiding a redundant CancellationTokenSource.
2026-07-02 13:17:43 -05:00
Philip Gai
b1eb6fd159 test: add regression and coverage tests for ACTIONS_CACHE_MODE
Add JobExtension L0 tests asserting the job-start cache-mode log line is
emitted for each mode and absent when the variable is unset, plus handler
regression tests covering coexistence with ACTIONS_CACHE_SERVICE_V2 and that
baseline runtime env is unaffected when no cache-mode is set.
2026-07-02 10:49:31 -05:00
Philip Gai
ab28939193 feat: expose effective Actions cache-mode to steps and job log
Export ACTIONS_CACHE_MODE env to node and container action steps when the
actions_cache_mode job variable is present and non-empty, mirroring the
existing ACTIONS_CACHE_SERVICE_V2 wiring. Also log the effective cache-mode
at job start. When the variable is absent or empty, behavior is unchanged.
2026-07-02 10:31:11 -05:00
Tingluo Huang
4c6d85cfc0 feat: enhance telemetry for action download resolution and failures (#4536) 2026-07-01 17:29:02 -04:00
github-actions[bot]
1ed4f70ee9 chore: update Node versions (#4530)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 10:27:28 -04:00
github-actions[bot]
c814d7ca46 chore: update Node versions (#4519)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-22 13:48:51 +00:00
github-actions[bot]
302ff10861 Update Docker to v29.6.0 and Buildx to v0.35.0 (#4516)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-22 13:35:29 +00:00
dependabot[bot]
74aa458a12 Bump actions/checkout from 6 to 7 (#4511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 16:20:11 +08:00
Tingluo Huang
c057cc3886 Report actions archive size in telemetry. (#4509) 2026-06-17 11:49:15 -04:00
Lokesh Gopu
16c52e389d Canceled background steps should not impact job result (#4482) 2026-06-08 17:18:11 -04:00
Francesco Renzi
060eeda6e0 Preparing runner release 2.335.0 (#4481)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-08 17:57:10 +01:00
23 changed files with 670 additions and 66 deletions

View File

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

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# 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@v6
- uses: actions/checkout@v7
- 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@v6
uses: actions/checkout@v7
- name: Check Docker version
id: check_docker_version
@@ -89,7 +89,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- 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@v6
uses: actions/checkout@v7
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@v6
uses: actions/checkout@v7
- 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@v6
- uses: actions/checkout@v7
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@v6
- uses: actions/checkout@v7
- 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@v6
- uses: actions/checkout@v7
- 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@v6
- uses: actions/checkout@v7
- 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@v6
- uses: actions/checkout@v7
# 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@v6
- uses: actions/checkout@v7
# Build runner layout
- name: Build & Layout Release
@@ -129,7 +129,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
# 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@v6
uses: actions/checkout@v7
- 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.5.3
ARG BUILDX_VERSION=0.34.1
ARG DOCKER_VERSION=29.6.0
ARG BUILDX_VERSION=0.35.0
RUN apt update -y && apt install curl unzip -y

View File

@@ -1,36 +1,40 @@
## What's Changed
* 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
* 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
## New Contributors
* @stefanpenner made their first contribution in https://github.com/actions/runner/pull/4296
* @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
**Full Changelog**: https://github.com/actions/runner/compare/v2.333.1...v2.334.0
**Full Changelog**: https://github.com/actions/runner/compare/v2.334.0...v2.335.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.16.0"
NODE24_VERSION="24.18.0"
get_abs_path() {
# exploits the fact that pwd will print abs path when no args

View File

@@ -228,7 +228,30 @@ namespace GitHub.Runner.Worker
{
throw new Exception($"Missing download info for {lookupKey}");
}
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
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)}"
});
}
}
// Parse action.yml and collect composite sub-actions for batched
@@ -398,7 +421,30 @@ namespace GitHub.Runner.Worker
if (repositoryActions.Count > 0)
{
// Get the download info
var downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions);
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)}"
});
}
// Download each action
foreach (var action in repositoryActions)
@@ -414,7 +460,29 @@ namespace GitHub.Runner.Worker
throw new Exception($"Missing download info for {lookupKey}");
}
await DownloadRepositoryActionAsync(executionContext, downloadInfo);
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)}"
});
}
}
// More preparation based on content in the repository (action.yml)
@@ -980,10 +1048,33 @@ namespace GitHub.Runner.Worker
if (actionsToResolve.Count > 0)
{
var downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve);
foreach (var kvp in downloadInfos)
IDictionary<string, WebApi.ActionDownloadInfo> downloadInfos = null;
Exception resolveFailure = null;
try
{
resolvedDownloadInfos[kvp.Key] = kvp.Value;
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)}"
});
}
}
}
@@ -1108,12 +1199,6 @@ 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);
@@ -1122,6 +1207,13 @@ 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
{
@@ -1159,7 +1251,6 @@ 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}.");
}
@@ -1209,6 +1300,12 @@ 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;

View File

@@ -32,6 +32,11 @@ namespace GitHub.Runner.Worker
// 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>
@@ -41,6 +46,7 @@ namespace GitHub.Runner.Worker
{
_backgroundSteps.Clear();
_completedStepIds.Clear();
_explicitlyCanceledStepIds.Clear();
var max = maxConcurrent > 0 ? maxConcurrent : DefaultMaxBackgroundSteps;
_backgroundSlotSemaphore = new SemaphoreSlim(max);
}
@@ -85,6 +91,9 @@ namespace GitHub.Runner.Worker
// 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();
@@ -95,14 +104,26 @@ namespace GitHub.Runner.Worker
CompleteWaitedSteps(unwaitedIds);
}
// Report the merged result of all background steps; the caller merges this into the job result.
var result = TaskResult.Succeeded;
foreach (var (_, (step, _, _)) in _backgroundSteps)
foreach (var (stepId, (step, _, _)) in _backgroundSteps)
{
if (step.ExecutionContext.Result.HasValue)
// 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)
{
result = TaskResultUtil.MergeTaskResults(result, step.ExecutionContext.Result.Value);
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)
@@ -256,6 +277,13 @@ namespace GitHub.Runner.Worker
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();

View File

@@ -239,6 +239,11 @@ namespace GitHub.Runner.Worker.Handlers
Environment["ACTIONS_RESULTS_URL"] = resultsUrl;
}
if (ExecutionContext.Global.Variables.TryGetValue("actions_cache_mode", out var cacheMode) && !string.IsNullOrEmpty(cacheMode))
{
Environment["ACTIONS_CACHE_MODE"] = cacheMode;
}
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
{
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))

View File

@@ -78,6 +78,11 @@ namespace GitHub.Runner.Worker.Handlers
Environment["ACTIONS_CACHE_SERVICE_V2"] = bool.TrueString;
}
if (ExecutionContext.Global.Variables.TryGetValue("actions_cache_mode", out var cacheMode) && !string.IsNullOrEmpty(cacheMode))
{
Environment["ACTIONS_CACHE_MODE"] = cacheMode;
}
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
{
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))

View File

@@ -171,6 +171,12 @@ namespace GitHub.Runner.Worker
context.Output($"Secret source: {secretSource}");
}
var cacheMode = jobContext.Global.Variables.Get("actions_cache_mode");
if (!string.IsNullOrEmpty(cacheMode))
{
context.Output($"Actions cache-mode: {cacheMode}");
}
var repoFullName = context.GetGitHubContext("repository");
ArgUtil.NotNull(repoFullName, nameof(repoFullName));
context.Debug($"Primary repository: {repoFullName}");

View File

@@ -90,6 +90,11 @@ 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
@@ -148,6 +153,11 @@ 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
{
@@ -215,6 +225,51 @@ 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")]
@@ -3333,6 +3388,16 @@ 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")]

View File

@@ -372,6 +372,88 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[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")]

View File

@@ -1,10 +1,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Actions.RunService.WebApi;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
@@ -85,5 +94,260 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("ubuntu:20.04", _stepTelemetry.Action);
}
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("read")]
[InlineData("none")]
[InlineData("write")]
[InlineData("write-only")]
public async Task RunAsync_ExportsCacheModeEnv_WhenVariableSet(string mode)
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_cache_mode", mode }
});
Assert.True(environment.TryGetValue("ACTIONS_CACHE_MODE", out var value));
Assert.Equal(mode, value);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_DoesNotExportCacheModeEnv_WhenVariableAbsent()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>());
Assert.False(environment.ContainsKey("ACTIONS_CACHE_MODE"));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_DoesNotExportCacheModeEnv_WhenVariableEmpty()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_cache_mode", "" }
});
Assert.False(environment.ContainsKey("ACTIONS_CACHE_MODE"));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_CacheModeCoexistsWithCacheServiceV2()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_uses_cache_service_v2", "true" },
{ "actions_cache_mode", "read" }
});
Assert.Equal(bool.TrueString, environment["ACTIONS_CACHE_SERVICE_V2"]);
Assert.Equal("read", environment["ACTIONS_CACHE_MODE"]);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_DoesNotAffectRuntimeEnv_WhenCacheModeAbsent()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>());
// Baseline runtime env is still exported and cache-mode adds nothing.
Assert.Equal("https://pipelines.actions.githubusercontent.com/", environment["ACTIONS_RUNTIME_URL"]);
Assert.Equal("token", environment["ACTIONS_RUNTIME_TOKEN"]);
Assert.False(environment.ContainsKey("ACTIONS_CACHE_MODE"));
Assert.False(environment.ContainsKey("ACTIONS_CACHE_SERVICE_V2"));
}
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("read")]
[InlineData("none")]
public async Task ContainerRunAsync_ExportsCacheModeEnv_WhenVariableSet(string mode)
{
// Container actions only run on Linux; RunAsync throws on other platforms.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return;
}
using (TestHostContext hc = CreateTestContext())
{
var container = await RunContainerActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_cache_mode", mode }
});
Assert.True(container.ContainerEnvironmentVariables.TryGetValue("ACTIONS_CACHE_MODE", out var value));
Assert.Equal(mode, value);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task ContainerRunAsync_DoesNotExportCacheModeEnv_WhenVariableAbsent()
{
// Container actions only run on Linux; RunAsync throws on other platforms.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return;
}
using (TestHostContext hc = CreateTestContext())
{
var container = await RunContainerActionHandlerAsync(hc, new Dictionary<string, VariableValue>());
Assert.False(container.ContainerEnvironmentVariables.ContainsKey("ACTIONS_CACHE_MODE"));
}
}
private async Task<ContainerInfo> RunContainerActionHandlerAsync(TestHostContext hc, IDictionary<string, VariableValue> variables)
{
// Route through the container-hooks path so the handler skips docker build/run.
variables[Constants.Runner.Features.AllowRunnerContainerHooks] = "true";
Environment.SetEnvironmentVariable(Constants.Hooks.ContainerHooksPath, Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "hooks.js"));
var tempDirectory = hc.GetDirectory(WellKnownDirectory.Temp);
Directory.CreateDirectory(Path.Combine(tempDirectory, "_runner_file_commands"));
Directory.CreateDirectory(Path.Combine(tempDirectory, "_github_workflow"));
var workspace = Path.Combine(hc.GetDirectory(WellKnownDirectory.Work), "workspace");
Directory.CreateDirectory(workspace);
var serverVariables = new Variables(hc, variables);
var endpoints = new List<ServiceEndpoint>
{
new ServiceEndpoint()
{
Name = WellKnownServiceEndpointNames.SystemVssConnection,
Url = new Uri("https://pipelines.actions.githubusercontent.com"),
Authorization = new EndpointAuthorization()
{
Scheme = "Test",
Parameters = { { "AccessToken", "token" } }
}
}
};
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
Endpoints = endpoints,
PrependPath = new List<string>(),
EnvironmentVariables = new Dictionary<string, string>()
});
_ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());
_ec.Setup(x => x.JobContext).Returns(new JobContext());
_ec.Setup(x => x.GetGitHubContext("workspace")).Returns(workspace);
ContainerInfo captured = null;
var hookManager = new Mock<IContainerHookManager>();
hookManager.Setup(x => x.RunContainerStepAsync(It.IsAny<IExecutionContext>(), It.IsAny<ContainerInfo>(), It.IsAny<string>()))
.Callback((IExecutionContext ec, ContainerInfo container, string dockerFile) => { captured = container; })
.Returns(Task.CompletedTask);
hc.SetSingleton(hookManager.Object);
hc.SetSingleton(new Mock<IActionManifestManagerWrapper>().Object);
var handler = new ContainerActionHandler();
handler.Initialize(hc);
handler.ExecutionContext = _ec.Object;
handler.Environment = new Dictionary<string, string>();
handler.Inputs = new Dictionary<string, string>();
handler.Action = new ContainerRegistryReference() { Image = "alpine:latest" };
handler.Data = new ContainerActionExecutionData() { Image = "docker://alpine:latest" };
await handler.RunAsync(ActionRunStage.Main);
return captured;
}
private async Task<Dictionary<string, string>> RunNodeScriptActionHandlerAsync(TestHostContext hc, IDictionary<string, VariableValue> variables)
{
var actionDirectory = Path.Combine(hc.GetDirectory(WellKnownDirectory.Work), Guid.NewGuid().ToString());
Directory.CreateDirectory(actionDirectory);
var scriptFile = "main.js";
File.WriteAllText(Path.Combine(actionDirectory, scriptFile), "// noop");
var serverVariables = new Variables(hc, variables);
var endpoints = new List<ServiceEndpoint>
{
new ServiceEndpoint()
{
Name = WellKnownServiceEndpointNames.SystemVssConnection,
Url = new Uri("https://pipelines.actions.githubusercontent.com"),
Authorization = new EndpointAuthorization()
{
Scheme = "Test",
Parameters = { { "AccessToken", "token" } }
}
}
};
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
Endpoints = endpoints,
PrependPath = new List<string>(),
EnvironmentVariables = new Dictionary<string, string>()
});
_ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());
_ec.Setup(x => x.GetGitHubContext("workspace")).Returns(actionDirectory);
_ec.Setup(x => x.GetMatchers()).Returns(new List<IssueMatcherConfig>());
_ec.Setup(x => x.ForceCompleted).Returns(new TaskCompletionSource<int>().Task);
_ec.Setup(x => x.CancellationToken).Returns(CancellationToken.None);
var stepHost = new Mock<IStepHost>();
stepHost.Setup(x => x.DetermineNodeRuntimeVersion(It.IsAny<IExecutionContext>(), It.IsAny<string>())).ReturnsAsync("node20");
stepHost.Setup(x => x.ResolvePathForStepHost(It.IsAny<IExecutionContext>(), It.IsAny<string>())).Returns((IExecutionContext ec, string path) => path);
stepHost.Setup(x => x.ExecuteAsync(
It.IsAny<IExecutionContext>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<bool>(),
It.IsAny<System.Text.Encoding>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>())).ReturnsAsync(0);
var handler = new NodeScriptActionHandler();
handler.Initialize(hc);
handler.ExecutionContext = _ec.Object;
handler.StepHost = stepHost.Object;
handler.Environment = new Dictionary<string, string>();
handler.Inputs = new Dictionary<string, string>();
handler.RuntimeVariables = serverVariables;
handler.ActionDirectory = actionDirectory;
handler.Action = new RepositoryPathReference() { Name = "actions/checkout", Ref = "v2" };
handler.Data = new NodeJSActionExecutionData() { Script = scriptFile, NodeVersion = "node20" };
await handler.RunAsync(ActionRunStage.Main);
return handler.Environment;
}
}
}

View File

@@ -238,6 +238,54 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("read")]
[InlineData("none")]
[InlineData("write")]
[InlineData("write-only")]
public async Task InitializeJob_LogsCacheMode_WhenVariableSet(string mode)
{
using (TestHostContext hc = CreateTestContext())
{
_jobEc.Global.Variables.Set("actions_cache_mode", mode);
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
await jobExtension.InitializeJob(_jobEc, _message);
_jobServerQueue.Verify(
x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(m => m.Contains($"Actions cache-mode: {mode}")), It.IsAny<long?>()),
Times.Once);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task InitializeJob_DoesNotLogCacheMode_WhenVariableAbsent()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
_actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny<IExecutionContext>(), It.IsAny<IEnumerable<Pipelines.JobStep>>(), It.IsAny<Guid>()))
.Returns(Task.FromResult(new PrepareResult(new List<JobExtensionRunner>(), new Dictionary<Guid, IActionRunner>())));
await jobExtension.InitializeJob(_jobEc, _message);
_jobServerQueue.Verify(
x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(m => m.Contains("Actions cache-mode:")), It.IsAny<long?>()),
Times.Never);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]

View File

@@ -1 +1 @@
2.334.0
2.335.0