Compare commits

..

1 Commits

Author SHA1 Message Date
Salman Chishti
f1995ede5d Update release version to 2.334.0 2026-04-21 19:49:15 +01:00
29 changed files with 118 additions and 1151 deletions

View File

@@ -1 +1 @@
<Update to ./src/runnerversion when creating release>
2.334.0

View File

@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@actions/glob": "^0.7.0"
"@actions/glob": "^0.6.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^5.10.0",
@@ -55,45 +55,13 @@
}
},
"node_modules/@actions/glob": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.7.0.tgz",
"integrity": "sha512-+7s3wM+cXapDLmLL1NVWHawqcJOZzXZy2df/VhNn8DnZtS/x83iTCKaUn9F0llur4h3CII0AilvKKH4CMPL8Gw==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.6.1.tgz",
"integrity": "sha512-K4+2Ac5ILcf2ySdJCha+Pop9NcKjxqCL4xL4zI50dgB2PbXgC0+AcP011xfH4Of6b4QEJJg8dyZYv7zl4byTsw==",
"license": "MIT",
"dependencies": {
"@actions/core": "^3.0.0",
"minimatch": "^10.2.5"
}
},
"node_modules/@actions/glob/node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@actions/glob/node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/@actions/glob/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"minimatch": "^3.0.4"
}
},
"node_modules/@actions/http-client": {
@@ -927,8 +895,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/big-integer": {
"version": "1.6.51",
@@ -955,7 +922,6 @@
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -1150,8 +1116,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@@ -3270,7 +3235,6 @@
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -4730,35 +4694,12 @@
}
},
"@actions/glob": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.7.0.tgz",
"integrity": "sha512-+7s3wM+cXapDLmLL1NVWHawqcJOZzXZy2df/VhNn8DnZtS/x83iTCKaUn9F0llur4h3CII0AilvKKH4CMPL8Gw==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.6.1.tgz",
"integrity": "sha512-K4+2Ac5ILcf2ySdJCha+Pop9NcKjxqCL4xL4zI50dgB2PbXgC0+AcP011xfH4Of6b4QEJJg8dyZYv7zl4byTsw==",
"requires": {
"@actions/core": "^3.0.0",
"minimatch": "^10.2.5"
},
"dependencies": {
"balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="
},
"brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"requires": {
"balanced-match": "^4.0.2"
}
},
"minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"requires": {
"brace-expansion": "^5.0.5"
}
}
"minimatch": "^3.0.4"
}
},
"@actions/http-client": {
@@ -5311,8 +5252,7 @@
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"big-integer": {
"version": "1.6.51",
@@ -5333,7 +5273,6 @@
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5450,8 +5389,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"cross-spawn": {
"version": "7.0.6",
@@ -6920,7 +6858,6 @@
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz",
"integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}

View File

@@ -32,7 +32,7 @@
"author": "GitHub Actions",
"license": "MIT",
"dependencies": {
"@actions/glob": "^0.7.0"
"@actions/glob": "^0.6.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^5.10.0",

View File

@@ -64,7 +64,6 @@ namespace GitHub.Runner.Common
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
private CancellationTokenSource _runnerShutdownTokenSource = new();
private object _perfLock = new();
private string _canonicalRootDirectory;
private Tracing _trace;
private Tracing _actionsHttpTrace;
private Tracing _netcoreHttpTrace;
@@ -392,12 +391,7 @@ namespace GitHub.Runner.Common
break;
case WellKnownDirectory.Root:
if (_canonicalRootDirectory == null)
{
_canonicalRootDirectory = PathUtil.GetCanonicalPath(
new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName);
}
path = _canonicalRootDirectory;
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName;
break;
case WellKnownDirectory.Temp:

View File

@@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.7" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.6" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@@ -12,6 +12,8 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
{
public class CheckoutTask : IRunnerActionPlugin
{
private readonly Regex _validSha1 = new(@"\b[0-9a-f]{40}\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, TimeSpan.FromSeconds(2));
public async Task RunAsync(RunnerActionPluginExecutionContext executionContext, CancellationToken token)
{
string runnerWorkspace = executionContext.GetRunnerContext("workspace");
@@ -97,7 +99,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_0
{
sourceBranch = refInput;
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.CommitHash))
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.SHA1))
{
sourceVersion = sourceBranch;

View File

@@ -96,7 +96,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_1
{
sourceBranch = refInput;
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.CommitHash))
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.SHA1))
{
sourceVersion = sourceBranch;
// If Ref is a SHA and the repo is self, we need to use github.ref as source branch since it might be refs/pull/*

View File

@@ -1,7 +1,4 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace GitHub.Runner.Sdk
{
@@ -9,98 +6,8 @@ namespace GitHub.Runner.Sdk
{
#if OS_WINDOWS
public static readonly string PathVariable = "Path";
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
System.IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
System.IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern uint GetFinalPathNameByHandle(
SafeFileHandle hFile,
[Out] StringBuilder lpszFilePath,
uint cchFilePath,
uint dwFlags);
private const uint FILE_READ_ATTRIBUTES = 0x80;
private const uint FILE_SHARE_READ = 0x1;
private const uint FILE_SHARE_WRITE = 0x2;
private const uint FILE_SHARE_DELETE = 0x4;
private const uint OPEN_EXISTING = 3;
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
private const uint VOLUME_NAME_DOS = 0x0;
/// <summary>
/// Returns the NTFS canonical path for a directory, resolving drive letter
/// and folder name casing to match what is stored on disk.
/// On non-Windows platforms, returns the path unchanged.
/// </summary>
public static string GetCanonicalPath(string path)
{
if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
{
return path;
}
using var handle = CreateFile(
path,
FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
System.IntPtr.Zero,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
System.IntPtr.Zero);
if (handle.IsInvalid)
{
return path;
}
var buffer = new StringBuilder(1024);
var result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
if (result == 0)
{
return path;
}
// Retry with a larger buffer if the path was longer than expected
if (result >= buffer.Capacity)
{
buffer = new StringBuilder((int)result + 1);
result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
if (result == 0 || result >= buffer.Capacity)
{
return path;
}
}
var canonicalPath = buffer.ToString();
// Strip the \\?\UNC\ prefix and convert to standard UNC path
if (canonicalPath.StartsWith(@"\\?\UNC\", System.StringComparison.Ordinal))
{
canonicalPath = @"\\" + canonicalPath.Substring(8);
}
// Strip the \\?\ prefix for local paths
else if (canonicalPath.StartsWith(@"\\?\", System.StringComparison.Ordinal))
{
canonicalPath = canonicalPath.Substring(4);
}
return canonicalPath;
}
#else
public static readonly string PathVariable = "PATH";
public static string GetCanonicalPath(string path)
{
return path;
}
#endif
public static string PrependPath(string path, string currentPath)

View File

@@ -880,11 +880,6 @@ namespace GitHub.Runner.Worker
return new Dictionary<string, WebApi.ActionDownloadInfo>();
}
// Pass lockfile dependencies to Launch when present, so it can
// perform ref-scoped policy matching with the original refs.
var deps = executionContext.Global.ActionsDependencies;
IList<string> dependencies = (deps != null && deps.Count > 0) ? deps : null;
// Resolve download info
var launchServer = HostContext.GetService<ILaunchServer>();
var jobServer = HostContext.GetService<IJobServer>();
@@ -896,7 +891,7 @@ namespace GitHub.Runner.Worker
if (MessageUtil.IsRunServiceJob(executionContext.Global.Variables.Get(Constants.Variables.System.JobRequestType)))
{
var displayHelpfulActionsDownloadErrors = executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.DisplayHelpfulActionsDownloadErrors) ?? false;
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences, Dependencies = dependencies }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
actionDownloadInfos = await launchServer.ResolveActionsDownloadInfoAsync(executionContext.Global.Plan.PlanId, executionContext.Root.Id, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken, displayHelpfulActionsDownloadErrors);
}
else
{
@@ -1446,11 +1441,6 @@ namespace GitHub.Runner.Worker
// It doesn't make sense to retry in this case, so just stop
throw new ActionNotFoundException(new Uri(downloadUrl), requestId);
}
else if (response.StatusCode == HttpStatusCode.Forbidden)
{
// It doesn't make sense to retry in this case, so just stop
throw new AccessDeniedException($"Access denied to '{downloadUrl}' ({requestId})");
}
else
{
// Something else bad happened, let's go to our retry logic
@@ -1474,11 +1464,6 @@ namespace GitHub.Runner.Worker
Trace.Info($"The action at '{downloadUrl}' does not exist");
throw;
}
catch (AccessDeniedException)
{
Trace.Info($"Access denied to '{downloadUrl}'");
throw;
}
catch (Exception ex) when (retryCount < 2)
{
retryCount++;
@@ -1504,7 +1489,7 @@ namespace GitHub.Runner.Worker
}
}
}
catch (Exception ex) when (!(ex is AccessDeniedException) && !(ex is OperationCanceledException) && !executionContext.CancellationToken.IsCancellationRequested)
catch (Exception ex) when (!(ex is OperationCanceledException) && !executionContext.CancellationToken.IsCancellationRequested)
{
Trace.Error($"Failed to download archive '{downloadUrl}' after {retryCount + 1} attempts.");
Trace.Error(ex);

View File

@@ -243,26 +243,6 @@ namespace GitHub.Runner.Worker.Dap
{
if (_state != DapSessionState.NotStarted)
{
// Pause so the user can inspect final job state before we tear down,
// but only if the user was stepping through (not if they hit continue).
if (IsActive && _pauseOnNextStep)
{
try
{
if (_jobContext != null)
{
Trace.Info("Job completed — pausing for inspection");
SendStoppedEvent("completed", "Job completed — inspect variables before the session ends.");
await WaitForCommandAsync(_jobContext.CancellationToken);
}
}
catch (Exception ex)
{
Trace.Warning($"DAP job-completed pause error: {ex.Message}");
}
}
try
{
OnJobCompleted();
@@ -272,6 +252,8 @@ namespace GitHub.Runner.Worker.Dap
Trace.Warning($"DAP OnJobCompleted error: {ex.Message}");
}
}
await StopAsync();
}
public async Task StopAsync()
@@ -1320,13 +1302,6 @@ namespace GitHub.Runner.Worker.Dap
_commandTcs = new TaskCompletionSource<DapCommand>(TaskCreationOptions.RunContinuationsAsynchronously);
}
// If cancellation already fired before we created the new TCS,
// the registration callback targeted the old one. Unblock now.
if (cancellationToken.IsCancellationRequested)
{
_commandTcs.TrySetResult(DapCommand.Disconnect);
}
Trace.Info("Waiting for debugger command...");
var command = await _commandTcs.Task;

View File

@@ -22,6 +22,5 @@ namespace GitHub.Runner.Worker.Dap
Task OnStepStartingAsync(IStep step);
void OnStepCompleted(IStep step);
Task OnJobCompletedAsync();
Task StopAsync();
}
}

View File

@@ -875,9 +875,6 @@ namespace GitHub.Runner.Worker
// File table
Global.FileTable = new List<String>(message.FileTable ?? new string[0]);
// Workflow dependencies (lockfile pins)
Global.ActionsDependencies = message.ActionsDependencies;
// What type of job request is running (i.e. Run Service vs. pipelines)
Global.Variables.Set(Constants.Variables.System.JobRequestType, message.MessageType);

View File

@@ -38,6 +38,5 @@ namespace GitHub.Runner.Worker
public HashSet<string> DeprecatedNode20Actions { get; set; }
public HashSet<string> UpgradedToNode24Actions { get; set; }
public HashSet<string> Arm32Node20Actions { get; set; }
public IList<String> ActionsDependencies { get; set; }
}
}

View File

@@ -16,7 +16,6 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Dap;
using GitHub.Services.Common;
using Newtonsoft.Json;
using Pipelines = GitHub.DistributedTask.Pipelines;
@@ -51,7 +50,6 @@ namespace GitHub.Runner.Worker
private Task _diskSpaceCheckTask = null;
private CancellationTokenSource _serviceConnectivityCheckToken = new();
private Task _serviceConnectivityCheckTask = null;
private IDapDebugger _dapDebugger;
// Download all required actions.
// Make sure all condition inputs are valid.
@@ -69,7 +67,6 @@ namespace GitHub.Runner.Worker
List<IStep> preJobSteps = new();
List<IStep> jobSteps = new();
var initSucceeded = false;
using (var register = jobContext.CancellationToken.Register(() => { context.CancelToken(); }))
{
try
@@ -80,25 +77,20 @@ namespace GitHub.Runner.Worker
var setting = HostContext.GetService<IConfigurationStore>().GetSettings();
var credFile = HostContext.GetConfigFile(WellKnownConfigFile.Credentials);
var credData = File.Exists(credFile) ? IOUtil.LoadObject<CredentialData>(credFile) : null;
// self-hosted runner is the only runner type using OAuth, can be identified via clientId
if (credData != null &&
credData.Data.TryGetValue("clientId", out _))
if (File.Exists(credFile))
{
context.Output($"Runner name: '{setting.AgentName}'");
// use system variable for group name since self-hosted runners can be renamed
if (message.Variables.TryGetValue("system.runnerGroupName", out VariableValue runnerGroupName))
var credData = IOUtil.LoadObject<CredentialData>(credFile);
if (credData != null &&
credData.Data.TryGetValue("clientId", out var clientId))
{
context.Output($"Runner group name: '{runnerGroupName.Value}'");
// print out HostName for self-hosted runner
context.Output($"Runner name: '{setting.AgentName}'");
if (message.Variables.TryGetValue("system.runnerGroupName", out VariableValue runnerGroupName))
{
context.Output($"Runner group name: '{runnerGroupName.Value}'");
}
context.Output($"Machine name: '{Environment.MachineName}'");
}
// print out machine name for self-hosted runner
context.Output($"Machine name: '{Environment.MachineName}'");
}
// print runner info for lhr runners, skips standard runners (PoolId = 0)
else if (setting.PoolId > 0 && !string.IsNullOrEmpty(setting.PoolName) && !string.IsNullOrEmpty(setting.AgentName))
{
context.Output($"Runner name: '{setting.AgentName}'");
context.Output($"Runner group name: '{setting.PoolName}'");
}
var setupInfoFile = HostContext.GetConfigFile(WellKnownConfigFile.SetupInfo);
@@ -484,41 +476,6 @@ namespace GitHub.Runner.Worker
Trace.Info($"Start checking service connectivity in background.");
_serviceConnectivityCheckTask = CheckServiceConnectivityAsync(context, _serviceConnectivityCheckToken.Token);
// Start the DAP debugger and wait for a client connection inside
// "Set up job" so the step stays in-progress while we wait.
if (jobContext.Global.Debugger?.Enabled == true)
{
Trace.Info("Debugger enabled — starting inside Set up job");
context.Output("Starting debugger…");
try
{
_dapDebugger = HostContext.GetService<IDapDebugger>();
await _dapDebugger.StartAsync(jobContext);
context.Output("Waiting for debugger client to connect…");
await _dapDebugger.WaitUntilReadyAsync();
context.Output("Debugger connected.");
AddDebuggerConnectionTelemetry(jobContext, "Connected");
}
catch (OperationCanceledException) when (jobContext.CancellationToken.IsCancellationRequested)
{
Trace.Info("Job was cancelled before debugger client connected.");
AddDebuggerConnectionTelemetry(jobContext, "Canceled");
context.Error("Job was cancelled before debugger client connected.");
throw;
}
catch (Exception ex)
{
Trace.Error($"DAP debugger failed: {ex.Message}");
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.GetType().Name}");
context.Error("The debugger failed to start or no debugger client connected in time.");
throw;
}
}
initSucceeded = true;
return steps;
}
catch (OperationCanceledException ex) when (jobContext.CancellationToken.IsCancellationRequested)
@@ -539,36 +496,12 @@ namespace GitHub.Runner.Worker
}
finally
{
// If InitializeJob failed after the debugger was started,
// tear down the transport here since FinalizeJob won't run.
if (!initSucceeded && _dapDebugger != null)
{
try
{
await _dapDebugger.StopAsync();
}
catch (Exception ex)
{
Trace.Warning($"DAP debugger cleanup during failed init: {ex.Message}");
}
_dapDebugger = null;
}
context.Debug("Finishing: Set up job");
context.Complete();
}
}
}
private static void AddDebuggerConnectionTelemetry(IExecutionContext jobContext, string result)
{
jobContext.Global.JobTelemetry.Add(new JobTelemetry
{
Type = JobTelemetryType.General,
Message = $"DebuggerConnectionResult: {result}"
});
}
private string GetWorkflowReference(IDictionary<string, VariableValue> variables)
{
var reference = "";
@@ -844,34 +777,6 @@ namespace GitHub.Runner.Worker
}
finally
{
// Pause for debugger inspection, then tear down the DAP session.
// OnJobCompletedAsync pauses first, then sends terminated/exited
// events and stops the transport.
if (_dapDebugger != null)
{
context.Output("Job completed — pausing for debugger inspection. Press continue to finish.");
try
{
await _dapDebugger.OnJobCompletedAsync();
}
catch (Exception ex)
{
Trace.Warning($"DAP debugger completion error: {ex.Message}");
}
finally
{
try
{
await _dapDebugger.StopAsync();
}
catch (Exception ex)
{
Trace.Warning($"DAP debugger stop error: {ex.Message}");
}
}
_dapDebugger = null;
}
context.Debug("Finishing: Complete job");
context.Complete();
}

View File

@@ -13,6 +13,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Dap;
using GitHub.Services.Common;
using GitHub.Services.WebApi;
using Sdk.RSWebApi.Contracts;
@@ -28,6 +29,7 @@ namespace GitHub.Runner.Worker
public sealed class JobRunner : RunnerService, IJobRunner
{
private const string DebuggerConnectionTelemetryPrefix = "DebuggerConnectionResult";
private IJobServerQueue _jobServerQueue;
private RunnerSettings _runnerSettings;
private ITempDirectoryManager _tempDirectoryManager;
@@ -112,6 +114,7 @@ namespace GitHub.Runner.Worker
IExecutionContext jobContext = null;
CancellationTokenRegistration? runnerShutdownRegistration = null;
IDapDebugger dapDebugger = null;
try
{
// Create the job execution context.
@@ -178,6 +181,25 @@ namespace GitHub.Runner.Worker
_tempDirectoryManager = HostContext.GetService<ITempDirectoryManager>();
_tempDirectoryManager.InitializeTempDirectory(jobContext);
// Setup the debugger
if (jobContext.Global.Debugger?.Enabled == true)
{
Trace.Info("Debugger enabled for this job run");
try
{
dapDebugger = HostContext.GetService<IDapDebugger>();
await dapDebugger.StartAsync(jobContext);
}
catch (Exception ex)
{
Trace.Error($"Failed to start DAP debugger: {ex.Message}");
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}");
jobContext.Error("Failed to start debugger.");
return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed);
}
}
// Get the job extension.
Trace.Info("Getting job extension.");
@@ -220,6 +242,33 @@ namespace GitHub.Runner.Worker
await Task.WhenAny(_jobServerQueue.JobRecordUpdated.Task, Task.Delay(1000));
}
// Wait for DAP debugger client connection and handshake after "Set up job"
// so the job page shows the setup step before we block on the debugger
if (dapDebugger != null)
{
try
{
await dapDebugger.WaitUntilReadyAsync();
AddDebuggerConnectionTelemetry(jobContext, "Connected");
}
catch (OperationCanceledException) when (jobRequestCancellationToken.IsCancellationRequested)
{
Trace.Info("Job was cancelled before debugger client connected.");
AddDebuggerConnectionTelemetry(jobContext, "Canceled");
jobContext.Error("Job was cancelled before debugger client connected.");
return await CompleteJobAsync(server, jobContext, message, TaskResult.Canceled);
}
catch (Exception ex)
{
Trace.Error($"DAP debugger failed to become ready: {ex.Message}");
AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}");
// If debugging was requested but the debugger is not available, fail the job
jobContext.Error("The debugger failed to start or no debugger client connected in time.");
return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed);
}
}
// Run all job steps
Trace.Info("Run all job steps.");
var stepsRunner = HostContext.GetService<IStepsRunner>();
@@ -260,6 +309,11 @@ namespace GitHub.Runner.Worker
runnerShutdownRegistration = null;
}
if (dapDebugger != null)
{
await dapDebugger.OnJobCompletedAsync();
}
await ShutdownQueue(throwOnFailure: false);
}
}
@@ -441,6 +495,15 @@ namespace GitHub.Runner.Worker
throw new AggregateException(exceptions);
}
private static void AddDebuggerConnectionTelemetry(IExecutionContext jobContext, string result)
{
jobContext.Global.JobTelemetry.Add(new JobTelemetry
{
Type = JobTelemetryType.General,
Message = $"{DebuggerConnectionTelemetryPrefix}: {result}"
});
}
private void MaskTelemetrySecrets(List<JobTelemetry> jobTelemetry)
{
foreach (var telemetryItem in jobTelemetry)

View File

@@ -267,21 +267,6 @@ namespace GitHub.DistributedTask.Pipelines
set;
}
/// <summary>
/// Gets the workflow-level action dependencies (lockfile entries)
/// </summary>
public IList<String> ActionsDependencies
{
get
{
if (m_actionsDependencies == null)
{
m_actionsDependencies = new List<String>();
}
return m_actionsDependencies;
}
}
/// <summary>
/// Gets the collection of variables associated with the current context.
/// </summary>
@@ -456,11 +441,6 @@ namespace GitHub.DistributedTask.Pipelines
m_variables = null;
}
if (m_actionsDependencies?.Count == 0)
{
m_actionsDependencies = null;
}
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
if (!string.IsNullOrEmpty(m_jobContainerResourceAlias))
{
@@ -486,9 +466,6 @@ namespace GitHub.DistributedTask.Pipelines
[DataMember(Name = "Variables", EmitDefaultValue = false)]
private IDictionary<String, VariableValue> m_variables;
[DataMember(Name = "dependencies", EmitDefaultValue = false)]
private List<String> m_actionsDependencies;
// todo: remove after feature-flag DistributedTask.EvaluateContainerOnRunner is enabled everywhere
[DataMember(Name = "JobSidecarContainers", EmitDefaultValue = false)]
private IDictionary<String, String> m_jobSidecarContainers;

View File

@@ -8,7 +8,6 @@ namespace GitHub.DistributedTask.Pipelines.Expressions
public const String Email = nameof(Email);
public const String IPv4Address = nameof(IPv4Address);
public const String SHA1 = nameof(SHA1);
public const String CommitHash = nameof(CommitHash);
public const String Url = nameof(Url);
/// <summary>
@@ -25,8 +24,7 @@ namespace GitHub.DistributedTask.Pipelines.Expressions
case IPv4Address:
return s_validIPv4Address;
case SHA1:
case CommitHash:
return s_validCommitHash;
return s_validSha1;
case Url:
return s_validUrl;
default:
@@ -48,9 +46,9 @@ namespace GitHub.DistributedTask.Pipelines.Expressions
)
);
// 40 or 64 hex characters (SHA-1 or SHA-256 commit hash)
private static readonly Lazy<Regex> s_validCommitHash = new Lazy<Regex>(() => new Regex(
@"\b(?:[0-9a-f]{40}|[0-9a-f]{64})\b",
// 40 hex characters
private static readonly Lazy<Regex> s_validSha1 = new Lazy<Regex>(() => new Regex(
@"\b[0-9a-f]{40}\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut()
)
);

View File

@@ -12,12 +12,5 @@ namespace GitHub.DistributedTask.WebApi
get;
set;
}
[DataMember(EmitDefaultValue = false)]
public IList<string> Dependencies
{
get;
set;
}
}
}

View File

@@ -22,9 +22,6 @@ namespace GitHub.Services.Launch.Contracts
{
[DataMember(EmitDefaultValue = false, Name = "actions")]
public IList<ActionReferenceRequest> Actions { get; set; }
[DataMember(EmitDefaultValue = false, Name = "actions_dependencies")]
public IList<string> ActionsDependencies { get; set; }
}
[DataContract]

View File

@@ -97,8 +97,7 @@ namespace GitHub.Services.Launch.Client
{
return new ActionReferenceRequestList
{
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList(),
ActionsDependencies = actionReferenceList.Dependencies
Actions = actionReferenceList.Actions?.Select(ToGitHubData).ToList()
};
}

View File

@@ -24,10 +24,7 @@ namespace GitHub.Runner.Common.Tests
"osx-arm64"
};
Assert.True(
BuildConstants.Source.CommitHash.Length == 40 || BuildConstants.Source.CommitHash.Length == 64,
"CommitHash should be a 40-char SHA-1 or 64-char SHA-256 hex string");
Assert.Matches("^[0-9a-f]+$", BuildConstants.Source.CommitHash);
Assert.Equal(40, BuildConstants.Source.CommitHash.Length);
Assert.True(validPackageNames.Contains(BuildConstants.RunnerPackage.PackageName), $"PackageName should be one of the following '{string.Join(", ", validPackageNames)}', current PackageName is '{BuildConstants.RunnerPackage.PackageName}'");
}
}

View File

@@ -299,52 +299,6 @@ namespace GitHub.Runner.Common.Tests
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetDirectoryRootReturnsCachedValue()
{
try
{
Setup();
// Call GetDirectory(Root) twice — should return the same reference
var root1 = _hc.GetDirectory(WellKnownDirectory.Root);
var root2 = _hc.GetDirectory(WellKnownDirectory.Root);
Assert.NotNull(root1);
Assert.Equal(root1, root2);
Assert.True(Directory.Exists(root1));
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetDirectoryDerivedPathsUseRootCasing()
{
try
{
Setup();
var root = _hc.GetDirectory(WellKnownDirectory.Root);
var diag = _hc.GetDirectory(WellKnownDirectory.Diag);
var externals = _hc.GetDirectory(WellKnownDirectory.Externals);
// Diag and Externals should start with the same Root prefix
Assert.StartsWith(root, diag);
Assert.StartsWith(root, externals);
}
finally
{
Teardown();
}
}
private void Setup([CallerMemberName] string testName = "")
{
_tokenSource = new CancellationTokenSource();

View File

@@ -14,7 +14,7 @@ using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Common.Tests.Listener
{
public sealed class RunnerL0 : IDisposable
public sealed class RunnerL0
{
private Mock<IConfigurationManager> _configurationManager;
private Mock<IJobNotification> _jobNotification;
@@ -29,7 +29,6 @@ namespace GitHub.Runner.Common.Tests.Listener
private Mock<ICredentialManager> _credentialManager;
private Mock<IActionsRunServer> _actionsRunServer;
private Mock<IRunServer> _runServer;
private readonly string _returnJobResultForHosted;
public RunnerL0()
{
@@ -46,14 +45,6 @@ namespace GitHub.Runner.Common.Tests.Listener
_credentialManager = new Mock<ICredentialManager>();
_actionsRunServer = new Mock<IActionsRunServer>();
_runServer = new Mock<IRunServer>();
_returnJobResultForHosted = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED");
Environment.SetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED", null);
}
public void Dispose()
{
Environment.SetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED", _returnJobResultForHosted);
}
private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName)

View File

@@ -119,48 +119,6 @@ public sealed class AgentJobRequestMessageL0
Assert.Null(recoveredMessage.DebuggerTunnel);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyActionsDependenciesDeserialization_WithDependencies()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string json = DoubleQuotify("{'dependencies': ['actions/checkout@v4:sha256-abc123', 'actions/setup-node@v4:sha256-def456']}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(json));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.Equal(2, recoveredMessage.ActionsDependencies.Count);
Assert.Equal("actions/checkout@v4:sha256-abc123", recoveredMessage.ActionsDependencies[0]);
Assert.Equal("actions/setup-node@v4:sha256-def456", recoveredMessage.ActionsDependencies[1]);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyActionsDependenciesDeserialization_DefaultsToEmpty()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string json = DoubleQuotify("{'messageType': 'PipelineAgentJobRequest'}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(json));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.Empty(recoveredMessage.ActionsDependencies);
}
private static string DoubleQuotify(string text)
{
return text.Replace('\'', '"');

View File

@@ -1,100 +0,0 @@
using GitHub.DistributedTask.Pipelines.Expressions;
using Xunit;
namespace GitHub.Runner.Common.Tests.Sdk
{
public sealed class WellKnownRegularExpressionsL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void SHA1_Key_Returns_CommitHash_Regex()
{
var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.SHA1);
Assert.NotNull(regex);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void CommitHash_Key_Returns_CommitHash_Regex()
{
var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash);
Assert.NotNull(regex);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void SHA1_And_CommitHash_Return_Same_Regex()
{
var sha1Regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.SHA1);
var commitHashRegex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash);
Assert.Same(sha1Regex, commitHashRegex);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void Matches_40_Char_Hex()
{
var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash);
Assert.Matches(regex.Value, new string('a', 40));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void Matches_64_Char_Hex()
{
var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash);
Assert.Matches(regex.Value, new string('a', 64));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void Does_Not_Match_63_Char_Hex()
{
var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash);
Assert.DoesNotMatch(regex.Value, new string('a', 63));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void Does_Not_Match_65_Char_Hex()
{
var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash);
Assert.DoesNotMatch(regex.Value, new string('a', 65));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void Matches_Mixed_Case_64_Char()
{
var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash);
var value = new string('A', 32) + new string('b', 32);
Assert.Matches(regex.Value, value);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Sdk")]
public void Unknown_Key_Returns_Null()
{
var regex = WellKnownRegularExpressions.GetRegex("UnknownType");
Assert.Null(regex);
}
}
}

View File

@@ -1,151 +0,0 @@
using GitHub.Runner.Sdk;
using System.IO;
using System.Runtime.InteropServices;
using Xunit;
namespace GitHub.Runner.Common.Tests.Util
{
public sealed class PathUtilL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsPath_WhenDirectoryDoesNotExist()
{
var fakePath = Path.Combine(Path.GetTempPath(), "nonexistent_" + Path.GetRandomFileName());
var result = PathUtil.GetCanonicalPath(fakePath);
Assert.Equal(fakePath, result);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsPath_WhenNull()
{
Assert.Null(PathUtil.GetCanonicalPath(null));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsEmpty_WhenEmpty()
{
Assert.Equal(string.Empty, PathUtil.GetCanonicalPath(string.Empty));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsValidPath_ForExistingDirectory()
{
var tempDir = Path.Combine(Path.GetTempPath(), "pathutil_test_" + Path.GetRandomFileName());
try
{
Directory.CreateDirectory(tempDir);
var result = PathUtil.GetCanonicalPath(tempDir);
Assert.NotNull(result);
Assert.True(Directory.Exists(result));
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir);
}
}
}
#if OS_WINDOWS
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_NormalizesDriveLetter_OnWindows()
{
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
// Skip if temp is a UNC path (no drive letter to normalize)
if (tempDir.StartsWith(@"\\"))
{
return;
}
// Force lowercase drive letter
var lowerCased = char.ToLower(tempDir[0]) + tempDir.Substring(1);
var result = PathUtil.GetCanonicalPath(lowerCased);
// The canonical path should have an uppercase drive letter
Assert.True(char.IsUpper(result[0]),
$"Expected uppercase drive letter but got: {result}");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_NormalizesFolderCasing_OnWindows()
{
// Create a directory with known casing, then query with wrong casing
var basePath = Path.GetTempPath();
if (basePath.StartsWith(@"\\"))
{
return; // Skip UNC
}
var realName = "PathUtilTest_MiXeDcAsE_" + Path.GetRandomFileName();
var realDir = Path.Combine(basePath, realName);
try
{
Directory.CreateDirectory(realDir);
// Query with all-lowercase version
var wrongCased = Path.Combine(basePath, realName.ToLowerInvariant());
var result = PathUtil.GetCanonicalPath(wrongCased);
// The canonical result should contain the original mixed-case name
Assert.Contains(realName, result);
}
finally
{
if (Directory.Exists(realDir))
{
Directory.Delete(realDir);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_IsIdempotent_OnWindows()
{
// Calling GetCanonicalPath twice should return the same result
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
var first = PathUtil.GetCanonicalPath(tempDir);
var second = PathUtil.GetCanonicalPath(first);
Assert.Equal(first, second);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsSameResult_RegardlessOfInputCasing_OnWindows()
{
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
if (tempDir.StartsWith(@"\\"))
{
return; // Skip UNC
}
var upper = tempDir.ToUpperInvariant();
var lower = tempDir.ToLowerInvariant();
var resultUpper = PathUtil.GetCanonicalPath(upper);
var resultLower = PathUtil.GetCanonicalPath(lower);
// Both should resolve to the same canonical path
Assert.Equal(resultUpper, resultLower);
}
#endif
}
}

View File

@@ -25,7 +25,6 @@ namespace GitHub.Runner.Common.Tests.Worker
public sealed class ActionManagerL0
{
private const string TestDataFolderName = "TestData";
private const string Sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private CancellationTokenSource _ecTokenSource;
private Mock<IConfigurationStore> _configurationStore;
private Mock<IDockerCommandManager> _dockerManager;
@@ -335,7 +334,7 @@ runs:
await File.WriteAllTextAsync(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact", "action.yml"), Content);
#if OS_WINDOWS
ZipFile.CreateFromDirectory(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact"), Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", $"{Sha256}.zip"), CompressionLevel.Fastest, true);
ZipFile.CreateFromDirectory(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact"), Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", "master-sha.zip"), CompressionLevel.Fastest, true);
#else
string tar = WhichUtil.Which("tar", require: true, trace: _hc.GetTrace());
@@ -361,7 +360,7 @@ runs:
string cwd = Path.GetDirectoryName(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact"));
string inputDirectory = Path.GetFileName(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact"));
string archiveFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", $"{Sha256}.tar.gz");
string archiveFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", "master-sha.tar.gz");
int exitCode = await processInvoker.ExecuteAsync(_hc.GetDirectory(WellKnownDirectory.Bin), tar, $"-czf \"{archiveFile}\" -C \"{cwd}\" \"{inputDirectory}\"", null, CancellationToken.None);
if (exitCode != 0)
{
@@ -369,8 +368,6 @@ runs:
}
}
#endif
MockResolvedSha("actions/download-artifact", "master", Sha256);
var actionId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
@@ -519,10 +516,9 @@ runs:
string actionsArchive = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions_archive", "action_checkout");
Directory.CreateDirectory(actionsArchive);
Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", Sha256));
Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", Sha256, "content"));
await File.WriteAllTextAsync(Path.Combine(actionsArchive, "actions_checkout", Sha256, "content", "action.yml"), Content);
MockResolvedSha("actions/checkout", "master", Sha256);
Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", "master-sha"));
Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", "master-sha", "content"));
await File.WriteAllTextAsync(Path.Combine(actionsArchive, "actions_checkout", "master-sha", "content", "action.yml"), Content);
Environment.SetEnvironmentVariable(Constants.Variables.Agent.ActionArchiveCacheDirectory, actionsArchive);
//Act
@@ -3153,51 +3149,6 @@ runs:
#endif
}
private void MockResolvedSha(string nameWithOwner, string reference, string resolvedSha)
{
_jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<Guid>(), It.IsAny<Guid>(), It.Is<ActionReferenceList>(actions => actions.Actions.Any(action => action.NameWithOwner == nameWithOwner && action.Ref == reference)), It.IsAny<CancellationToken>()))
.Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)
{
var key = $"{action.NameWithOwner}@{action.Ref}";
result.Actions[key] = new ActionDownloadInfo
{
NameWithOwner = action.NameWithOwner,
Ref = action.Ref,
ResolvedNameWithOwner = action.NameWithOwner,
ResolvedSha = resolvedSha,
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
};
}
return Task.FromResult(result);
});
_launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.Is<ActionReferenceList>(actions => actions.Actions.Any(action => action.NameWithOwner == nameWithOwner && action.Ref == reference)), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)
{
var key = $"{action.NameWithOwner}@{action.Ref}";
result.Actions[key] = new ActionDownloadInfo
{
NameWithOwner = action.NameWithOwner,
Ref = action.Ref,
ResolvedNameWithOwner = action.NameWithOwner,
ResolvedSha = resolvedSha,
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
};
}
return Task.FromResult(result);
});
}
private void Setup([CallerMemberName] string name = "", bool enableComposite = true)
{
_ecTokenSource?.Dispose();
@@ -3332,141 +3283,5 @@ runs:
Directory.Delete(_workFolder, recursive: true);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task GetDownloadInfoAsync_PropagatesDependencies_WhenPresent()
{
try
{
// Arrange
Setup();
// Set RunServiceJob so we hit the Launch path
_ec.Object.Global.Variables.Set(Constants.Variables.System.JobRequestType, "RunnerJobRequest");
// Populate lockfile dependencies
_ec.Object.Global.ActionsDependencies = new List<string>
{
"github.com/actions/checkout@v4:sha256-abc123",
"github.com/actions/setup-node@v4:sha256-def456"
};
// Capture the ActionReferenceList passed to Launch
ActionReferenceList capturedList = null;
_launchServer
.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.Callback<Guid, Guid, ActionReferenceList, CancellationToken, bool>((planId, jobId, list, ct, display) => capturedList = list)
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken ct, bool display) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)
{
var key = $"{action.NameWithOwner}@{action.Ref}";
result.Actions[key] = new ActionDownloadInfo
{
NameWithOwner = action.NameWithOwner,
Ref = action.Ref,
ResolvedNameWithOwner = action.NameWithOwner,
ResolvedSha = $"{action.Ref}-sha",
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
};
}
return Task.FromResult(result);
});
var actionStep = new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/checkout",
Ref = "v4",
RepositoryType = "GitHub"
}
};
// Act
var result = await _actionManager.PrepareActionsAsync(_ec.Object, new List<Pipelines.JobStep> { actionStep }, default);
// Assert
Assert.NotNull(capturedList);
Assert.NotNull(capturedList.Dependencies);
Assert.Equal(2, capturedList.Dependencies.Count);
Assert.Equal("github.com/actions/checkout@v4:sha256-abc123", capturedList.Dependencies[0]);
Assert.Equal("github.com/actions/setup-node@v4:sha256-def456", capturedList.Dependencies[1]);
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task GetDownloadInfoAsync_OmitsDependencies_WhenEmpty()
{
try
{
// Arrange
Setup();
// Set RunServiceJob so we hit the Launch path
_ec.Object.Global.Variables.Set(Constants.Variables.System.JobRequestType, "RunnerJobRequest");
// No dependencies set (default empty list from GlobalContext)
// Capture the ActionReferenceList passed to Launch
ActionReferenceList capturedList = null;
_launchServer
.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny<Guid>(), It.IsAny<Guid>(), It.IsAny<ActionReferenceList>(), It.IsAny<CancellationToken>(), It.IsAny<bool>()))
.Callback<Guid, Guid, ActionReferenceList, CancellationToken, bool>((planId, jobId, list, ct, display) => capturedList = list)
.Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken ct, bool display) =>
{
var result = new ActionDownloadInfoCollection { Actions = new Dictionary<string, ActionDownloadInfo>() };
foreach (var action in actions.Actions)
{
var key = $"{action.NameWithOwner}@{action.Ref}";
result.Actions[key] = new ActionDownloadInfo
{
NameWithOwner = action.NameWithOwner,
Ref = action.Ref,
ResolvedNameWithOwner = action.NameWithOwner,
ResolvedSha = $"{action.Ref}-sha",
TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}",
ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}",
};
}
return Task.FromResult(result);
});
var actionStep = new Pipelines.ActionStep()
{
Name = "action",
Id = Guid.NewGuid(),
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/checkout",
Ref = "v4",
RepositoryType = "GitHub"
}
};
// Act
var result = await _actionManager.PrepareActionsAsync(_ec.Object, new List<Pipelines.JobStep> { actionStep }, default);
// Assert
Assert.NotNull(capturedList);
Assert.Null(capturedList.Dependencies);
}
finally
{
Teardown();
}
}
}
}

View File

@@ -744,32 +744,14 @@ namespace GitHub.Runner.Common.Tests.Worker
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
await waitTask;
// Complete the job — OnJobCompletedAsync pauses when stepping,
// so run it in the background and send continue to unblock.
var completedTask = _debugger.OnJobCompletedAsync();
// Complete the job — events are sent via OnJobCompletedAsync
await _debugger.OnJobCompletedAsync();
// Read the stopped event from the pause
var stoppedMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"event\":\"stopped\"", stoppedMsg);
var msg1 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
var msg2 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
// Send continue to unblock the pause
await SendRequestAsync(stream, new Request
{
Seq = 2,
Type = "request",
Command = "continue"
});
await completedTask;
// Read remaining messages — continue response + continued event + terminated + exited
var allMessages = new System.Text.StringBuilder();
for (int i = 0; i < 4; i++)
{
allMessages.Append(await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)));
}
var combined = allMessages.ToString();
// Both events should arrive (order may vary)
var combined = msg1 + msg2;
Assert.Contains("\"event\":\"terminated\"", combined);
Assert.Contains("\"event\":\"exited\"", combined);
}
@@ -827,45 +809,5 @@ namespace GitHub.Runner.Common.Tests.Worker
});
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WaitForCommandAsyncUnblocksOnCancellationDuringWait()
{
using (CreateTestContext())
{
var port = GetFreePort();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
await _debugger.StartAsync(jobContext.Object);
var waitTask = _debugger.WaitUntilReadyAsync();
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
await SendRequestAsync(stream, new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
await waitTask;
// Start OnJobCompletedAsync — it will pause because _pauseOnNextStep is true
var completedTask = _debugger.OnJobCompletedAsync();
// Read the stopped event
var stoppedMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"event\":\"stopped\"", stoppedMsg);
// Cancel the job while waiting — should unblock the pause
cts.Cancel();
// OnJobCompletedAsync should complete without hanging
var finished = await Task.WhenAny(completedTask, Task.Delay(TimeSpan.FromSeconds(5)));
Assert.Equal(completedTask, finished);
}
}
}
}

View File

@@ -760,171 +760,5 @@ namespace GitHub.Runner.Common.Tests.Worker
Environment.SetEnvironmentVariable("RUNNER_ENVIRONMENT", null);
Environment.SetEnvironmentVariable("GITHUB_ACTIONS_IMAGE_GEN_ENABLED", null);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DebuggerStartedInSetupJobWhenEnabled()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
// Enable debugger on the message
_message.EnableDebugger = true;
_message.DebuggerTunnel = new Pipelines.DebuggerTunnelInfo
{
TunnelId = "test-tunnel",
ClusterId = "test-cluster",
HostToken = "test-token",
Port = 9229
};
// Re-initialize the execution context so it picks up debugger config
_jobEc = new Runner.Worker.ExecutionContext();
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
// Set up mock debugger
var mockDebugger = new Mock<IDapDebugger>();
mockDebugger.Setup(x => x.StartAsync(It.IsAny<IExecutionContext>())).Returns(Task.CompletedTask);
mockDebugger.Setup(x => x.WaitUntilReadyAsync()).Returns(Task.CompletedTask);
hc.SetSingleton(mockDebugger.Object);
_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>())));
List<IStep> result = await jobExtension.InitializeJob(_jobEc, _message);
// Verify DAP debugger was started and waited on
mockDebugger.Verify(x => x.StartAsync(It.IsAny<IExecutionContext>()), Times.Once);
mockDebugger.Verify(x => x.WaitUntilReadyAsync(), Times.Once);
// Verify steps are still returned correctly
Assert.Equal(5, result.Count);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DebuggerNotStartedInSetupJobWhenDisabled()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
// Debugger NOT enabled on the message — should not be started
// Set up mock debugger (should NOT be called)
var mockDebugger = new Mock<IDapDebugger>();
hc.SetSingleton(mockDebugger.Object);
_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>())));
List<IStep> result = await jobExtension.InitializeJob(_jobEc, _message);
// Verify DAP debugger was NOT started during setup job
mockDebugger.Verify(x => x.StartAsync(It.IsAny<IExecutionContext>()), Times.Never);
mockDebugger.Verify(x => x.WaitUntilReadyAsync(), Times.Never);
Assert.Equal(5, result.Count);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DebuggerCleanedUpInFinalizeJob()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
// Enable debugger on the message
_message.EnableDebugger = true;
_message.DebuggerTunnel = new Pipelines.DebuggerTunnelInfo
{
TunnelId = "test-tunnel",
ClusterId = "test-cluster",
HostToken = "test-token",
Port = 9229
};
// Re-initialize the execution context so it picks up debugger config
_jobEc = new Runner.Worker.ExecutionContext();
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
// Set up mock debugger
var mockDebugger = new Mock<IDapDebugger>();
mockDebugger.Setup(x => x.StartAsync(It.IsAny<IExecutionContext>())).Returns(Task.CompletedTask);
mockDebugger.Setup(x => x.WaitUntilReadyAsync()).Returns(Task.CompletedTask);
mockDebugger.Setup(x => x.OnJobCompletedAsync()).Returns(Task.CompletedTask);
hc.SetSingleton(mockDebugger.Object);
_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>())));
// Run InitializeJob to start the debugger
await jobExtension.InitializeJob(_jobEc, _message);
// Run FinalizeJob — should pause (inside OnJobCompletedAsync) then clean up
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
// Verify OnJobCompletedAsync was called (it handles pause + cleanup)
mockDebugger.Verify(x => x.OnJobCompletedAsync(), Times.Once);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task FinalizeJobHandlesDebuggerCleanupException()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);
// Enable debugger on the message
_message.EnableDebugger = true;
_message.DebuggerTunnel = new Pipelines.DebuggerTunnelInfo
{
TunnelId = "test-tunnel",
ClusterId = "test-cluster",
HostToken = "test-token",
Port = 9229
};
// Re-initialize the execution context so it picks up debugger config
_jobEc = new Runner.Worker.ExecutionContext();
_jobEc.Initialize(hc);
_jobEc.InitializeJob(_message, _tokenSource.Token);
// Set up mock debugger — OnJobCompletedAsync throws
var mockDebugger = new Mock<IDapDebugger>();
mockDebugger.Setup(x => x.StartAsync(It.IsAny<IExecutionContext>())).Returns(Task.CompletedTask);
mockDebugger.Setup(x => x.WaitUntilReadyAsync()).Returns(Task.CompletedTask);
mockDebugger.Setup(x => x.OnJobCompletedAsync()).ThrowsAsync(new InvalidOperationException("tunnel disposed"));
mockDebugger.Setup(x => x.StopAsync()).Returns(Task.CompletedTask);
hc.SetSingleton(mockDebugger.Object);
_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);
// FinalizeJob should not throw even when OnJobCompletedAsync throws
await jobExtension.FinalizeJob(_jobEc, _message, DateTime.UtcNow);
mockDebugger.Verify(x => x.OnJobCompletedAsync(), Times.Once);
mockDebugger.Verify(x => x.StopAsync(), Times.Once);
}
}
}
}