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 ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
|
||||||
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
|
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 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";
|
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)
|
public static void Delete(string path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
DeleteDirectory(path, cancellationToken);
|
DeleteDirectory(path, cancellationToken);
|
||||||
|
|||||||
@@ -773,10 +773,6 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// make sure we get a clean folder ready to use.
|
|
||||||
IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken);
|
|
||||||
Directory.CreateDirectory(destDirectory);
|
|
||||||
|
|
||||||
if (downloadInfo.PackageDetails != null)
|
if (downloadInfo.PackageDetails != null)
|
||||||
{
|
{
|
||||||
executionContext.Output($"##[group]Download immutable action package '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'");
|
executionContext.Output($"##[group]Download immutable action package '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'");
|
||||||
@@ -811,6 +807,50 @@ namespace GitHub.Runner.Worker
|
|||||||
if (!string.IsNullOrEmpty(actionArchiveCacheDir) &&
|
if (!string.IsNullOrEmpty(actionArchiveCacheDir) &&
|
||||||
Directory.Exists(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;
|
hasActionArchiveCache = true;
|
||||||
Trace.Info($"Check if action archive '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}' already exists in cache directory '{actionArchiveCacheDir}'");
|
Trace.Info($"Check if action archive '{downloadInfo.ResolvedNameWithOwner}@{downloadInfo.ResolvedSha}' already exists in cache directory '{actionArchiveCacheDir}'");
|
||||||
#if OS_WINDOWS
|
#if OS_WINDOWS
|
||||||
@@ -892,6 +932,10 @@ namespace GitHub.Runner.Worker
|
|||||||
}
|
}
|
||||||
#endif
|
#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
|
// repository archive from github always contains a nested folder
|
||||||
var subDirectories = new DirectoryInfo(stagingDirectory).GetDirectories();
|
var subDirectories = new DirectoryInfo(stagingDirectory).GetDirectories();
|
||||||
if (subDirectories.Length != 1)
|
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
|
#if OS_LINUX
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
|
|||||||
Reference in New Issue
Block a user