mirror of
https://github.com/actions/runner.git
synced 2026-03-12 17:57:13 -04:00
Symlink actions cache (#4260)
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user