Compare commits

..

6 Commits

Author SHA1 Message Date
Salman Muin Kayser Chishti
9433bf68ab Sort deprecated actions list for deterministic annotation output 2026-02-11 13:47:09 +00:00
Salman Muin Kayser Chishti
143730289c Clarify env vars can be set on runner or in workflow file 2026-02-11 13:46:29 +00:00
Salman Muin Kayser Chishti
7caf050db3 Add opt-in/opt-out environment variable guidance to deprecation warning 2026-02-11 13:45:51 +00:00
Salman Muin Kayser Chishti
f19c4ee70e Improve deprecation message: list actions running on node20, suggest checking for updates 2026-02-11 13:43:46 +00:00
Salman Muin Kayser Chishti
af8c4aa59d Fix deprecation message wording: actions will be forced to node24, not stop working 2026-02-11 13:42:29 +00:00
Salman Muin Kayser Chishti
bf5f154d63 Add Node.js 20 deprecation warning annotation
When the actions.runner.warnonnode20 feature flag is enabled, the runner
collects all actions using Node.js 20 (including node12/16 that get
migrated to node20) during the job and emits a single warning annotation
at job finalization:

"Node.js 20 actions are deprecated and will stop working on June 2nd,
2025. Please update the following actions to use Node.js 24: {actions}.
For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/"

Also adds the blog post link to the Phase 2 (node24 default) info message.
2026-02-11 13:39:16 +00:00
27 changed files with 327 additions and 304 deletions

View File

@@ -4,7 +4,7 @@
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
"ghcr.io/devcontainers/features/dotnet": {
"version": "8.0.417"
"version": "8.0.416"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"

View File

@@ -28,8 +28,8 @@ Debian based OS (Debian, Ubuntu, Linux Mint)
- liblttng-ust1 or liblttng-ust0
- libkrb5-3
- zlib1g
- libssl3t64, libssl3, libssl1.1, libssl1.0.2 or libssl1.0.0
- libicu76, libicu75, ..., libicu66, libicu65, libicu63, libicu60, libicu57, libicu55, or libicu52
- libssl1.1, libssl1.0.2 or libssl1.0.0
- libicu63, libicu60, libicu57 or libicu55
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)

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.2.0
ARG BUILDX_VERSION=0.31.1
ARG DOCKER_VERSION=29.0.2
ARG BUILDX_VERSION=0.30.1
RUN apt update -y && apt install curl unzip -y
@@ -21,7 +21,7 @@ RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-c
&& unzip ./runner-container-hooks.zip -d ./k8s \
&& rm runner-container-hooks.zip
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.1/actions-runner-hooks-k8s-0.8.1.zip \
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v0.8.0/actions-runner-hooks-k8s-0.8.0.zip \
&& unzip ./runner-container-hooks.zip -d ./k8s-novolume \
&& rm runner-container-hooks.zip

View File

@@ -1,27 +1,30 @@
## What's Changed
* Fix owner of /home/runner directory by @nikola-jokic in https://github.com/actions/runner/pull/4132
* Update Docker to v29.0.2 and Buildx to v0.30.1 by @github-actions[bot] in https://github.com/actions/runner/pull/4135
* Update workflow around runner docker image. by @TingluoHuang in https://github.com/actions/runner/pull/4133
* Fix regex for validating runner version format by @TingluoHuang in https://github.com/actions/runner/pull/4136
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4144
* Ensure safe_sleep tries alternative approaches by @TingluoHuang in https://github.com/actions/runner/pull/4146
* Bump actions/github-script from 7 to 8 by @dependabot[bot] in https://github.com/actions/runner/pull/4137
* Bump actions/checkout from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4130
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4149
* Bump docker image to use ubuntu 24.04 by @TingluoHuang in https://github.com/actions/runner/pull/4018
* Add support for case function by @AllanGuigou in https://github.com/actions/runner/pull/4147
* Cleanup feature flag actions_container_action_runner_temp by @ericsciple in https://github.com/actions/runner/pull/4163
* Bump actions/download-artifact from 6 to 7 by @dependabot[bot] in https://github.com/actions/runner/pull/4155
* Bump actions/upload-artifact from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4157
* Set ACTIONS_ORCHESTRATION_ID as env to actions. by @TingluoHuang in https://github.com/actions/runner/pull/4178
* Allow hosted VM report job telemetry via .setup_info file. by @TingluoHuang in https://github.com/actions/runner/pull/4186
* Bump typescript from 5.9.2 to 5.9.3 in /src/Misc/expressionFunc/hashFiles by @dependabot[bot] in https://github.com/actions/runner/pull/4184
* Bump Azure.Storage.Blobs from 12.26.0 to 12.27.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4189
* Custom Image: Preflight checks by @lawrencegripper in https://github.com/actions/runner/pull/4081
* Update dotnet sdk to latest version @8.0.415 by @github-actions[bot] in https://github.com/actions/runner/pull/4080
* Link to an extant discussion category by @jsoref in https://github.com/actions/runner/pull/4084
* Improve logic around decide IsHostedServer. by @TingluoHuang in https://github.com/actions/runner/pull/4086
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4093
* Compare updated template evaluator by @ericsciple in https://github.com/actions/runner/pull/4092
* fix(dockerfile): set more lenient permissions on /home/runner by @caxu-rh in https://github.com/actions/runner/pull/4083
* Add support for libicu73-76 for newer Debian/Ubuntu versions by @lets-build-an-ocean in https://github.com/actions/runner/pull/4098
* Bump actions/download-artifact from 5 to 6 by @dependabot[bot] in https://github.com/actions/runner/pull/4089
* Bump actions/upload-artifact from 4 to 5 by @dependabot[bot] in https://github.com/actions/runner/pull/4088
* Bump Azure.Storage.Blobs from 12.25.1 to 12.26.0 by @dependabot[bot] in https://github.com/actions/runner/pull/4077
* Only start runner after network is online by @dupondje in https://github.com/actions/runner/pull/4094
* Retry http error related to DNS resolution failure. by @TingluoHuang in https://github.com/actions/runner/pull/4110
* Update Docker to v29.0.1 and Buildx to v0.30.0 by @github-actions[bot] in https://github.com/actions/runner/pull/4114
* chore: update Node versions by @github-actions[bot] in https://github.com/actions/runner/pull/4115
* Update dotnet sdk to latest version @8.0.416 by @github-actions[bot] in https://github.com/actions/runner/pull/4116
* Compare updated workflow parser for ActionManifestManager by @ericsciple in https://github.com/actions/runner/pull/4111
* Bump npm pkg version for hashFiles. by @TingluoHuang in https://github.com/actions/runner/pull/4122
## New Contributors
* @AllanGuigou made their first contribution in https://github.com/actions/runner/pull/4147
* @lawrencegripper made their first contribution in https://github.com/actions/runner/pull/4081
* @caxu-rh made their first contribution in https://github.com/actions/runner/pull/4083
* @lets-build-an-ocean made their first contribution in https://github.com/actions/runner/pull/4098
* @dupondje made their first contribution in https://github.com/actions/runner/pull/4094
**Full Changelog**: https://github.com/actions/runner/compare/v2.330.0...v2.331.0
**Full Changelog**: https://github.com/actions/runner/compare/v2.329.0...v2.330.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

@@ -6,8 +6,8 @@ NODE_URL=https://nodejs.org/dist
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.0"
NODE24_VERSION="24.13.0"
NODE20_VERSION="20.19.6"
NODE24_VERSION="24.12.0"
get_abs_path() {
# exploits the fact that pwd will print abs path when no args

View File

@@ -102,7 +102,7 @@ then
exit 1
fi
apt_get_with_fallbacks libssl3t64$ libssl3$ libssl1.1$ libssl1.0.2$ libssl1.0.0$
apt_get_with_fallbacks libssl1.1$ libssl1.0.2$ libssl1.0.0$
if [ $? -ne 0 ]
then
echo "'$apt_get' failed with exit code '$?'"

View File

@@ -173,7 +173,6 @@ namespace GitHub.Runner.Common
public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check";
public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser";
public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions";
public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations";
}
// Node version migration related constants
@@ -190,6 +189,10 @@ namespace GitHub.Runner.Common
// Feature flags for controlling the migration phases
public static readonly string UseNode24ByDefaultFlag = "actions.runner.usenode24bydefault";
public static readonly string RequireNode24Flag = "actions.runner.requirenode24";
public static readonly string WarnOnNode20Flag = "actions.runner.warnonnode20";
// Blog post URL for Node 20 deprecation
public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/";
}
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";

View File

@@ -24,7 +24,7 @@ namespace GitHub.Runner.Listener
public interface IJobDispatcher : IRunnerService
{
bool Busy { get; }
TaskCompletionSource<TaskResult> RunOnceJobCompleted { get; }
TaskCompletionSource<bool> RunOnceJobCompleted { get; }
void Run(Pipelines.AgentJobRequestMessage message, bool runOnce = false);
bool Cancel(JobCancelMessage message);
Task WaitAsync(CancellationToken token);
@@ -56,7 +56,7 @@ namespace GitHub.Runner.Listener
// timeout limit can be overwritten by environment GITHUB_ACTIONS_RUNNER_CHANNEL_TIMEOUT
private TimeSpan _channelTimeout;
private TaskCompletionSource<TaskResult> _runOnceJobCompleted = new();
private TaskCompletionSource<bool> _runOnceJobCompleted = new();
public event EventHandler<JobStatusEventArgs> JobStatus;
@@ -82,7 +82,7 @@ namespace GitHub.Runner.Listener
Trace.Info($"Set runner/worker IPC timeout to {_channelTimeout.TotalSeconds} seconds.");
}
public TaskCompletionSource<TaskResult> RunOnceJobCompleted => _runOnceJobCompleted;
public TaskCompletionSource<bool> RunOnceJobCompleted => _runOnceJobCompleted;
public bool Busy { get; private set; }
@@ -340,19 +340,18 @@ namespace GitHub.Runner.Listener
private async Task RunOnceAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
{
var jobResult = TaskResult.Succeeded;
try
{
jobResult = await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
await RunAsync(message, orchestrationId, previousJobDispatch, jobRequestCancellationToken, workerCancelTimeoutKillToken);
}
finally
{
Trace.Info("Fire signal for one time used runner.");
_runOnceJobCompleted.TrySetResult(jobResult);
_runOnceJobCompleted.TrySetResult(true);
}
}
private async Task<TaskResult> RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
private async Task RunAsync(Pipelines.AgentJobRequestMessage message, string orchestrationId, WorkerDispatcher previousJobDispatch, CancellationToken jobRequestCancellationToken, CancellationToken workerCancelTimeoutKillToken)
{
Busy = true;
try
@@ -400,7 +399,7 @@ namespace GitHub.Runner.Listener
{
// renew job request task complete means we run out of retry for the first job request renew.
Trace.Info($"Unable to renew job request for job {message.JobId} for the first time, stop dispatching job to worker.");
return TaskResult.Abandoned;
return;
}
if (jobRequestCancellationToken.IsCancellationRequested)
@@ -413,7 +412,7 @@ namespace GitHub.Runner.Listener
// complete job request with result Cancelled
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, TaskResult.Canceled);
return TaskResult.Canceled;
return;
}
HostContext.WritePerfCounter($"JobRequestRenewed_{requestId.ToString()}");
@@ -524,7 +523,7 @@ namespace GitHub.Runner.Listener
await renewJobRequest;
// not finish the job request since the job haven't run on worker at all, we will not going to set a result to server.
return TaskResult.Failed;
return;
}
// we get first jobrequest renew succeed and start the worker process with the job message.
@@ -605,7 +604,7 @@ namespace GitHub.Runner.Listener
Trace.Error(detailInfo);
}
return TaskResultUtil.TranslateFromReturnCode(returnCode);
return;
}
else if (completedTask == renewJobRequest)
{
@@ -707,8 +706,6 @@ namespace GitHub.Runner.Listener
// complete job request
await CompleteJobRequestAsync(_poolId, message, systemConnection, lockToken, resultOnAbandonOrCancel);
return resultOnAbandonOrCancel;
}
finally
{

View File

@@ -5,8 +5,8 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -324,11 +324,8 @@ namespace GitHub.Runner.Listener
HostContext.EnableAuthMigration("EnableAuthMigrationByDefault");
}
// hosted runner only run one job and would like to know the result of the job for telemetry and alerting on failure spike.
var returnJobResultForHosted = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED"));
// Run the runner interactively or as service
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral || returnJobResultForHosted, returnJobResultForHosted);
return await ExecuteRunnerAsync(settings, command.RunOnce || settings.Ephemeral);
}
else
{
@@ -404,32 +401,17 @@ namespace GitHub.Runner.Listener
}
//create worker manager, create message listener and start listening to the queue
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false, bool returnRunOnceJobResult = false)
private async Task<int> RunAsync(RunnerSettings settings, bool runOnce = false)
{
try
{
Trace.Info(nameof(RunAsync));
// Validate directory permissions.
string workDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
Trace.Info($"Validating directory permissions for: '{workDirectory}'");
try
{
Directory.CreateDirectory(workDirectory);
IOUtil.ValidateExecutePermission(workDirectory);
}
catch (Exception ex)
{
Trace.Error(ex);
_term.WriteError($"Fail to create and validate runner's work directory '{workDirectory}'.");
return Constants.Runner.ReturnCode.TerminatedError;
}
// First try using migrated settings if available
var configManager = HostContext.GetService<IConfigurationManager>();
RunnerSettings migratedSettings = null;
try
try
{
migratedSettings = configManager.LoadMigratedSettings();
Trace.Info("Loaded migrated settings from .runner_migrated file");
@@ -440,15 +422,15 @@ namespace GitHub.Runner.Listener
// If migrated settings file doesn't exist or can't be loaded, we'll use the provided settings
Trace.Info($"Failed to load migrated settings: {ex.Message}");
}
bool usedMigratedSettings = false;
if (migratedSettings != null)
{
// Try to create session with migrated settings first
Trace.Info("Attempting to create session using migrated settings");
_listener = GetMessageListener(migratedSettings, isMigratedSettings: true);
try
{
CreateSessionResult createSessionResult = await _listener.CreateSessionAsync(HostContext.RunnerShutdownToken);
@@ -468,7 +450,7 @@ namespace GitHub.Runner.Listener
Trace.Error($"Exception when creating session with migrated settings: {ex}");
}
}
// If migrated settings weren't used or session creation failed, use original settings
if (!usedMigratedSettings)
{
@@ -521,7 +503,7 @@ namespace GitHub.Runner.Listener
restartSession = true;
break;
}
TaskAgentMessage message = null;
bool skipMessageDeletion = false;
try
@@ -583,21 +565,6 @@ namespace GitHub.Runner.Listener
Trace.Info($"Ignore any exception after cancel message loop. {ex}");
}
if (returnRunOnceJobResult)
{
try
{
var jobResult = await jobDispatcher.RunOnceJobCompleted.Task;
return TaskResultUtil.TranslateToReturnCode(jobResult);
}
catch (Exception ex)
{
Trace.Error("run once job finished with error.");
Trace.Error(ex);
return Constants.Runner.ReturnCode.TerminatedError;
}
}
return Constants.Runner.ReturnCode.Success;
}
}
@@ -884,15 +851,15 @@ namespace GitHub.Runner.Listener
return Constants.Runner.ReturnCode.Success;
}
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce, bool returnRunOnceJobResult)
private async Task<int> ExecuteRunnerAsync(RunnerSettings settings, bool runOnce)
{
int returnCode = Constants.Runner.ReturnCode.Success;
bool restart = false;
do
{
restart = false;
returnCode = await RunAsync(settings, runOnce, returnRunOnceJobResult);
returnCode = await RunAsync(settings, runOnce);
if (returnCode == Constants.Runner.ReturnCode.RunnerConfigurationRefreshed)
{
Trace.Info("Runner configuration was refreshed, restarting session...");

View File

@@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@@ -318,17 +318,6 @@ namespace GitHub.Runner.Worker
context.AddIssue(issue, ExecutionContextLogOptions.Default);
}
if (!context.Global.HasDeprecatedSetOutput)
{
context.Global.HasDeprecatedSetOutput = true;
var telemetry = new JobTelemetry
{
Type = JobTelemetryType.ActionCommand,
Message = "DeprecatedCommand: set-output"
};
context.Global.JobTelemetry.Add(telemetry);
}
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
{
throw new Exception("Required field 'name' is missing in ##[set-output] command.");
@@ -364,17 +353,6 @@ namespace GitHub.Runner.Worker
context.AddIssue(issue, ExecutionContextLogOptions.Default);
}
if (!context.Global.HasDeprecatedSaveState)
{
context.Global.HasDeprecatedSaveState = true;
var telemetry = new JobTelemetry
{
Type = JobTelemetryType.ActionCommand,
Message = "DeprecatedCommand: save-state"
};
context.Global.JobTelemetry.Add(telemetry);
}
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
{
throw new Exception("Required field 'name' is missing in ##[save-state] command.");

View File

@@ -379,14 +379,7 @@ namespace GitHub.Runner.Worker
{
prefix = PipelineTemplateConstants.RunDisplayPrefix;
var repositoryReference = action.Reference as RepositoryPathReference;
var pathString = string.Empty;
if (!string.IsNullOrEmpty(repositoryReference.Path))
{
// For local actions (Name is empty), don't prepend "/" to avoid "/./"
pathString = string.IsNullOrEmpty(repositoryReference.Name)
? repositoryReference.Path
: $"/{repositoryReference.Path}";
}
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
tokenToParse = new StringToken(null, null, null, repoString);

View File

@@ -499,7 +499,7 @@ namespace GitHub.Runner.Worker
PublishStepTelemetry();
if (_record.RecordType == ExecutionContextType.Task)
if (_record.RecordType == "Task")
{
var stepResult = new StepResult
{
@@ -532,25 +532,6 @@ namespace GitHub.Runner.Worker
Global.StepsResult.Add(stepResult);
}
if (Global.Variables.GetBoolean(Constants.Runner.Features.SendJobLevelAnnotations) ?? false)
{
if (_record.RecordType == ExecutionContextType.Job)
{
_record.Issues?.ForEach(issue =>
{
var annotation = issue.ToAnnotation();
if (annotation != null)
{
Global.JobAnnotations.Add(annotation.Value);
if (annotation.Value.IsInfrastructureIssue && string.IsNullOrEmpty(Global.InfrastructureFailureCategory))
{
Global.InfrastructureFailureCategory = issue.Category;
}
}
});
}
}
if (Root != this)
{
// only dispose TokenSource for step level ExecutionContext
@@ -856,6 +837,9 @@ namespace GitHub.Runner.Worker
// Job level annotations
Global.JobAnnotations = new List<Annotation>();
// Track Node.js 20 actions for deprecation warning
Global.DeprecatedNode20Actions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Job Outputs
JobOutputs = new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);

View File

@@ -31,7 +31,6 @@ namespace GitHub.Runner.Worker
public JObject ContainerHookState { get; set; }
public bool HasTemplateEvaluatorMismatch { get; set; }
public bool HasActionManifestMismatch { get; set; }
public bool HasDeprecatedSetOutput { get; set; }
public bool HasDeprecatedSaveState { get; set; }
public HashSet<string> DeprecatedNode20Actions { get; set; }
}
}

View File

@@ -65,6 +65,20 @@ namespace GitHub.Runner.Worker.Handlers
nodeData.NodeVersion = Common.Constants.Runner.NodeMigration.Node20;
}
// Track Node.js 20 actions for deprecation annotation
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
{
bool warnOnNode20 = executionContext.Global.Variables?.GetBoolean(Constants.Runner.NodeMigration.WarnOnNode20Flag) ?? false;
if (warnOnNode20)
{
string actionName = GetActionName(action);
if (!string.IsNullOrEmpty(actionName))
{
executionContext.Global.DeprecatedNode20Actions?.Add(actionName);
}
}
}
// Check if node20 was explicitly specified in the action
// We don't modify if node24 was explicitly specified
if (string.Equals(nodeData.NodeVersion, Constants.Runner.NodeMigration.Node20, StringComparison.InvariantCultureIgnoreCase))
@@ -90,7 +104,8 @@ namespace GitHub.Runner.Worker.Handlers
if (useNode24ByDefault && !requireNode24 && string.Equals(finalNodeVersion, Constants.Runner.NodeMigration.Node24, StringComparison.OrdinalIgnoreCase))
{
string infoMessage = "Node 20 is being deprecated. This workflow is running with Node 24 by default. " +
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable.";
"If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. " +
$"For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
executionContext.Output(infoMessage);
}
}
@@ -129,5 +144,18 @@ namespace GitHub.Runner.Worker.Handlers
handler.LocalActionContainerSetupSteps = localActionContainerSetupSteps;
return handler;
}
private static string GetActionName(Pipelines.ActionStepDefinitionReference action)
{
if (action is Pipelines.RepositoryPathReference repoRef)
{
var pathString = string.IsNullOrEmpty(repoRef.Path) ? string.Empty : $"/{repoRef.Path}";
return string.IsNullOrEmpty(repoRef.Ref)
? $"{repoRef.Name}{pathString}"
: $"{repoRef.Name}{pathString}@{repoRef.Ref}";
}
return null;
}
}
}

View File

@@ -735,6 +735,15 @@ namespace GitHub.Runner.Worker
context.Global.JobTelemetry.Add(new JobTelemetry() { Type = JobTelemetryType.ConnectivityCheck, Message = $"Fail to check service connectivity. {ex.Message}" });
}
}
// Add deprecation warning annotation for Node.js 20 actions
if (context.Global.DeprecatedNode20Actions?.Count > 0)
{
var sortedActions = context.Global.DeprecatedNode20Actions.OrderBy(a => a, StringComparer.OrdinalIgnoreCase);
var actionsList = string.Join(", ", sortedActions);
var deprecationMessage = $"Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: {actionsList}. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2025. Please check if updated versions of these actions are available that support Node.js 24. To opt into Node.js 24 now, set the FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the runner or in your workflow file. Once Node.js 24 becomes the default, you can temporarily opt out by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see: {Constants.Runner.NodeMigration.Node20DeprecationUrl}";
context.Warning(deprecationMessage);
}
}
catch (Exception ex)
{

View File

@@ -421,7 +421,7 @@
"mapping": {
"properties": {
"image": "string",
"options": "string",
"options": "non-empty-string",
"env": "container-env",
"ports": "sequence-of-non-empty-string",
"volumes": "sequence-of-non-empty-string",

View File

@@ -18,19 +18,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.27.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.26.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<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.2" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="8.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<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.2" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -2593,7 +2593,7 @@
"description": "Use `jobs.<job_id>.container.image` to define the Docker image to use as the container to run the action. The value can be the Docker Hub image or a registry name."
},
"options": {
"type": "string",
"type": "non-empty-string",
"description": "Use `jobs.<job_id>.container.options` to configure additional Docker container resource options."
},
"env": "container-env",

View File

@@ -739,8 +739,7 @@ namespace GitHub.Runner.Common.Tests.Listener
Assert.True(jobDispatcher.RunOnceJobCompleted.Task.IsCompleted, "JobDispatcher should set task complete token for one time agent.");
if (jobDispatcher.RunOnceJobCompleted.Task.IsCompleted)
{
var result = await jobDispatcher.RunOnceJobCompleted.Task;
Assert.Equal(TaskResult.Succeeded, result);
Assert.True(await jobDispatcher.RunOnceJobCompleted.Task, "JobDispatcher should set task complete token to 'TRUE' for one time agent.");
}
}
}

View File

@@ -295,13 +295,13 @@ namespace GitHub.Runner.Common.Tests.Listener
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
var runOnceJobCompleted = new TaskCompletionSource<bool>();
_jobDispatcher.Setup(x => x.RunOnceJobCompleted)
.Returns(runOnceJobCompleted);
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
.Callback(() =>
{
runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
runOnceJobCompleted.TrySetResult(true);
});
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
@@ -399,13 +399,13 @@ namespace GitHub.Runner.Common.Tests.Listener
_messageListener.Setup(x => x.DeleteMessageAsync(It.IsAny<TaskAgentMessage>()))
.Returns(Task.CompletedTask);
var runOnceJobCompleted = new TaskCompletionSource<TaskResult>();
var runOnceJobCompleted = new TaskCompletionSource<bool>();
_jobDispatcher.Setup(x => x.RunOnceJobCompleted)
.Returns(runOnceJobCompleted);
_jobDispatcher.Setup(x => x.Run(It.IsAny<Pipelines.AgentJobRequestMessage>(), It.IsAny<bool>()))
.Callback(() =>
{
runOnceJobCompleted.TrySetResult(TaskResult.Succeeded);
runOnceJobCompleted.TrySetResult(true);
});
_jobNotification.Setup(x => x.StartClient(It.IsAny<String>()))
.Callback(() =>
@@ -733,8 +733,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(TaskResult.Succeeded);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act
@@ -834,8 +834,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(TaskResult.Succeeded);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act
@@ -954,8 +954,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
var completedTask = new TaskCompletionSource<TaskResult>();
completedTask.SetResult(TaskResult.Succeeded);
var completedTask = new TaskCompletionSource<bool>();
completedTask.SetResult(true);
_jobDispatcher.Setup(x => x.RunOnceJobCompleted).Returns(completedTask);
//Act

View File

@@ -457,8 +457,6 @@ namespace GitHub.Runner.Common.Tests.Worker
new SetEnvCommandExtension(),
new WarningCommandExtension(),
new AddMaskCommandExtension(),
new SetOutputCommandExtension(),
new SaveStateCommandExtension(),
};
foreach (var command in commands)
{
@@ -501,53 +499,5 @@ namespace GitHub.Runner.Common.Tests.Worker
};
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputCommand_EmitsTelemetryOnce()
{
using (TestHostContext hc = CreateTestContext())
{
_ec.Object.Global.JobTelemetry = new List<JobTelemetry>();
var reference = string.Empty;
_ec.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference));
// First set-output should add telemetry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::set-output name=foo::bar", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
Assert.Equal(JobTelemetryType.ActionCommand, _ec.Object.Global.JobTelemetry[0].Type);
Assert.Equal("DeprecatedCommand: set-output", _ec.Object.Global.JobTelemetry[0].Message);
Assert.True(_ec.Object.Global.HasDeprecatedSetOutput);
// Second set-output should not add another telemetry entry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::set-output name=foo2::bar2", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SaveStateCommand_EmitsTelemetryOnce()
{
using (TestHostContext hc = CreateTestContext())
{
_ec.Object.Global.JobTelemetry = new List<JobTelemetry>();
_ec.Setup(x => x.IsEmbedded).Returns(false);
_ec.Setup(x => x.IntraActionState).Returns(new Dictionary<string, string>());
// First save-state should add telemetry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::save-state name=foo::bar", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
Assert.Equal(JobTelemetryType.ActionCommand, _ec.Object.Global.JobTelemetry[0].Type);
Assert.Equal("DeprecatedCommand: save-state", _ec.Object.Global.JobTelemetry[0].Message);
Assert.True(_ec.Object.Global.HasDeprecatedSaveState);
// Second save-state should not add another telemetry entry
Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::save-state name=foo2::bar2", null));
Assert.Single(_ec.Object.Global.JobTelemetry);
}
}
}
}

View File

@@ -316,94 +316,6 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("${{ matrix.node }}", _actionRunner.DisplayName);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateDisplayNameForLocalAction()
{
// Arrange
Setup();
var actionId = Guid.NewGuid();
var action = new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
Path = "./"
}
};
_actionRunner.Action = action;
// Act
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
// Assert
Assert.True(validDisplayName);
Assert.True(updated);
Assert.Equal("Run ./", _actionRunner.DisplayName); // NOT "Run /./"
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateDisplayNameForLocalActionWithPath()
{
// Arrange
Setup();
var actionId = Guid.NewGuid();
var action = new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
RepositoryType = Pipelines.PipelineConstants.SelfAlias,
Path = "./.github/actions/my-action"
}
};
_actionRunner.Action = action;
// Act
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
// Assert
Assert.True(validDisplayName);
Assert.True(updated);
Assert.Equal("Run ./.github/actions/my-action", _actionRunner.DisplayName);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EvaluateDisplayNameForRemoteActionWithPath()
{
// Arrange
Setup();
var actionId = Guid.NewGuid();
var action = new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
Name = "owner/repo",
Path = "subdir",
Ref = "v1"
}
};
_actionRunner.Action = action;
// Act
var validDisplayName = _actionRunner.EvaluateDisplayName(_context, _actionRunner.ExecutionContext, out bool updated);
// Assert
Assert.True(validDisplayName);
Assert.True(updated);
Assert.Equal("Run owner/repo/subdir@v1", _actionRunner.DisplayName);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -547,7 +459,7 @@ namespace GitHub.Runner.Common.Tests.Worker
_handlerFactory = new Mock<IHandlerFactory>();
_defaultStepHost = new Mock<IDefaultStepHost>();
var actionManifestLegacy = new ActionManifestManagerLegacy();
actionManifestLegacy.Initialize(_hc);
_hc.SetSingleton<IActionManifestManagerLegacy>(actionManifestLegacy);

View File

@@ -74,7 +74,7 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
@@ -116,5 +116,206 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("node24", handler.Data.NodeVersion);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_TrackedWhenWarnFlagEnabled()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v4"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
);
// Assert.
Assert.Contains("actions/checkout@v4", deprecatedActions);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node20Action_NotTrackedWhenWarnFlagDisabled()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>();
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v4"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node20";
hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
);
// Assert - should not track when flag is disabled
Assert.Empty(deprecatedActions);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node24Action_NotTrackedEvenWhenWarnFlagEnabled()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "actions/checkout",
Ref = "v5"
};
// Act.
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node24";
hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
);
// Assert - node24 actions should not be tracked
Assert.Empty(deprecatedActions);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void Node12Action_TrackedAsDeprecatedWhenWarnFlagEnabled()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange.
var hf = new HandlerFactory();
hf.Initialize(hc);
var variables = new Dictionary<string, VariableValue>
{
{ Constants.Runner.NodeMigration.WarnOnNode20Flag, new VariableValue("true") }
};
Variables serverVariables = new(hc, variables);
var deprecatedActions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
EnvironmentVariables = new Dictionary<string, string>(),
DeprecatedNode20Actions = deprecatedActions
});
var actionRef = new RepositoryPathReference
{
Name = "some-org/old-action",
Ref = "v1"
};
// Act - node12 gets migrated to node20, then should be tracked
var data = new NodeJSActionExecutionData();
data.NodeVersion = "node12";
hf.Create(
_ec.Object,
actionRef,
new Mock<IStepHost>().Object,
data,
new Dictionary<string, string>(),
new Dictionary<string, string>(),
new Variables(hc, new Dictionary<string, VariableValue>()),
"",
new List<JobExtensionRunner>()
);
// Assert - node12 gets migrated to node20 and should be tracked
Assert.Contains("some-org/old-action@v1", deprecatedActions);
}
}
}
}

View File

@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
PACKAGE_DIR="$SCRIPT_DIR/../_package"
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
DOTNETSDK_VERSION="8.0.417"
DOTNETSDK_VERSION="8.0.416"
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
RUNNER_VERSION=$(cat runnerversion)

View File

@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.417"
"version": "8.0.416"
}
}

View File

@@ -1 +1 @@
2.331.0
2.330.0