Symlink actions cache (#4260)

This commit is contained in:
Pavel Iakovenko
2026-02-24 12:19:46 -05:00
committed by GitHub
parent ecb5f298fa
commit 052dfbdd68
4 changed files with 126 additions and 4 deletions

View File

@@ -289,6 +289,7 @@ namespace GitHub.Runner.Common
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
public static readonly string ActionArchiveCacheDirectory = "ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE";
public static readonly string SymlinkCachedActions = "ACTIONS_RUNNER_SYMLINK_CACHED_ACTIONS";
public static readonly string EmitCompositeMarkers = "ACTIONS_RUNNER_EMIT_COMPOSITE_MARKERS";
}

View File

@@ -93,6 +93,16 @@ namespace GitHub.Runner.Sdk
}
}
public static FileSystemInfo CreateSymbolicLink(string destDirectory, string srcDirectory)
{
// ensure directory chain exists
Directory.CreateDirectory(destDirectory);
// delete leaf directory
Directory.Delete(destDirectory);
// create symlink for the leaf directory
return Directory.CreateSymbolicLink(destDirectory, srcDirectory);
}
public static void Delete(string path, CancellationToken cancellationToken)
{
DeleteDirectory(path, cancellationToken);

View File

@@ -773,10 +773,6 @@ namespace GitHub.Runner.Worker
}
else
{
// make sure we get a clean folder ready to use.
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
Directory.CreateDirectory(destDirectory);
if (downloadInfo.PackageDetails != null)
{
executionContext.Output($"##[group]Download immutable action package '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'");
@@ -811,6 +807,50 @@ namespace GitHub.Runner.Worker
if (!string.IsNullOrEmpty(actionArchiveCacheDir) &&
Directory.Exists(actionArchiveCacheDir))
{
var symlinkCachedActions = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Agent.SymlinkCachedActions));
if (symlinkCachedActions)
{
Trace.Info($"Checking if can symlink '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}'");
var cacheDirectory = Path.Combine(actionArchiveCacheDir, downloadInfo.ResolvedNameWithOwner.Replace(Path.DirectorySeparatorChar, '_').Replace(Path.AltDirectorySeparatorChar, '_'), downloadInfo.ResolvedSha);
if (Directory.Exists(cacheDirectory))
{
try
{
Trace.Info($"Found unpacked action directory '{cacheDirectory}' in cache directory '{actionArchiveCacheDir}'");
// repository archive from github always contains a nested folder
var nestedDirectories = new DirectoryInfo(cacheDirectory).GetDirectories();
if (nestedDirectories.Length != 1)
{
throw new InvalidOperationException($"'{cacheDirectory}' contains '{nestedDirectories.Length}' directories");
}
else
{
executionContext.Debug($"Symlink '{nestedDirectories[0].Name}' to '{destDirectory}'");
// make sure we get a clean folder ready to use.
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
IOUtil.CreateSymbolicLink(destDirectory, nestedDirectories[0].FullName);
}
executionContext.Debug($"Created symlink from cached directory '{cacheDirectory}' to '{destDirectory}'");
executionContext.Global.JobTelemetry.Add(new JobTelemetry()
{
Type = JobTelemetryType.General,
Message = $"Action archive cache usage: {downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha} use cache {useActionArchiveCache} has cache {hasActionArchiveCache} via symlink"
});
Trace.Info("Finished getting action repository.");
return;
}
catch (Exception ex)
{
Trace.Error($"Failed to create symlink from cached directory '{cacheDirectory}' to '{destDirectory}'. Error: {ex}");
// Fall through to normal download logic
}
}
}
hasActionArchiveCache = true;
Trace.Info($"Check if action archive '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}' already exists in cache directory '{actionArchiveCacheDir}'");
#if OS_WINDOWS
@@ -892,6 +932,10 @@ namespace GitHub.Runner.Worker
}
#endif
// make sure we get a clean folder ready to use.
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
Directory.CreateDirectory(destDirectory);
// repository archive from github always contains a nested folder
var subDirectories = new DirectoryInfo(stagingDirectory).GetDirectories();
if (subDirectories.Length != 1)

View File

@@ -468,6 +468,73 @@ runs:
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async void PrepareActions_SymlinkCacheIsReentrant()
{
try
{
//Arrange
Environment.SetEnvironmentVariable(Constants.Variables.Agent.SymlinkCachedActions, "true");
Setup();
var actionId = Guid.NewGuid();
var actions = new List<Pipelines.ActionStep>
{
new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/checkout",
Ref = "master",
RepositoryType = "GitHub"
}
},
new Pipelines.ActionStep()
{
Name = "action",
Id = actionId,
Reference = new Pipelines.RepositoryPathReference()
{
Name = "actions/checkout",
Ref = "master",
RepositoryType = "GitHub"
}
}
};
const string Content = @"
name: 'Test'
runs:
using: 'node20'
main: 'dist/index.js'
";
string actionsArchive = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions_archive", "action_checkout");
Directory.CreateDirectory(actionsArchive);
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
await _actionManager.PrepareActionsAsync(_ec.Object, actions);
//Assert
string destDirectory = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions", "checkout", "master");
Assert.True(Directory.Exists(destDirectory), "Destination directory does not exist");
var di = new DirectoryInfo(destDirectory);
Assert.NotNull(di.LinkTarget);
}
finally
{
Environment.SetEnvironmentVariable(Constants.Variables.Agent.SymlinkCachedActions, null);
Teardown();
}
}
#if OS_LINUX
[Fact]
[Trait("Level", "L0")]