Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Runner.Worker/Handlers/ContainerActionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ public async Task RunAsync(ActionRunStage stage)
Environment["ACTIONS_RESULTS_URL"] = resultsUrl;
}

if (ExecutionContext.Global.Variables.TryGetValue("actions_cache_mode", out var cacheMode) && !string.IsNullOrEmpty(cacheMode))
{
Environment["ACTIONS_CACHE_MODE"] = cacheMode;
}
Comment thread
philip-gai marked this conversation as resolved.

if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
{
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
Expand Down
5 changes: 5 additions & 0 deletions src/Runner.Worker/Handlers/NodeScriptActionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public async Task RunAsync(ActionRunStage stage)
Environment["ACTIONS_CACHE_SERVICE_V2"] = bool.TrueString;
}

if (ExecutionContext.Global.Variables.TryGetValue("actions_cache_mode", out var cacheMode) && !string.IsNullOrEmpty(cacheMode))
{
Environment["ACTIONS_CACHE_MODE"] = cacheMode;
}

if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.SetOrchestrationIdEnvForActions) ?? false)
{
if (ExecutionContext.Global.Variables.TryGetValue(Constants.Variables.System.OrchestrationId, out var orchestrationId) && !string.IsNullOrEmpty(orchestrationId))
Expand Down
6 changes: 6 additions & 0 deletions src/Runner.Worker/JobExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ public async Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipel
context.Output($"Secret source: {secretSource}");
}

var cacheMode = jobContext.Global.Variables.Get("actions_cache_mode");
if (!string.IsNullOrEmpty(cacheMode))
{
context.Output($"Actions cache-mode: {cacheMode}");
}

var repoFullName = context.GetGitHubContext("repository");
ArgUtil.NotNull(repoFullName, nameof(repoFullName));
context.Debug($"Primary repository: {repoFullName}");
Expand Down
264 changes: 264 additions & 0 deletions src/Test/L0/Worker/HandlerL0.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Actions.RunService.WebApi;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
Expand Down Expand Up @@ -85,5 +94,260 @@ public void PrepareExecution_PopulateTelemetry_DockerActions()
Assert.Equal("ubuntu:20.04", _stepTelemetry.Action);
}
}

[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("read")]
[InlineData("none")]
[InlineData("write")]
[InlineData("write-only")]
public async Task RunAsync_ExportsCacheModeEnv_WhenVariableSet(string mode)
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_cache_mode", mode }
});

Assert.True(environment.TryGetValue("ACTIONS_CACHE_MODE", out var value));
Assert.Equal(mode, value);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_DoesNotExportCacheModeEnv_WhenVariableAbsent()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>());

Assert.False(environment.ContainsKey("ACTIONS_CACHE_MODE"));
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_DoesNotExportCacheModeEnv_WhenVariableEmpty()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_cache_mode", "" }
});

Assert.False(environment.ContainsKey("ACTIONS_CACHE_MODE"));
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_CacheModeCoexistsWithCacheServiceV2()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_uses_cache_service_v2", "true" },
{ "actions_cache_mode", "read" }
});

Assert.Equal(bool.TrueString, environment["ACTIONS_CACHE_SERVICE_V2"]);
Assert.Equal("read", environment["ACTIONS_CACHE_MODE"]);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task RunAsync_DoesNotAffectRuntimeEnv_WhenCacheModeAbsent()
{
using (TestHostContext hc = CreateTestContext())
{
var environment = await RunNodeScriptActionHandlerAsync(hc, new Dictionary<string, VariableValue>());

// Baseline runtime env is still exported and cache-mode adds nothing.
Assert.Equal("https://pipelines.actions.githubusercontent.com/", environment["ACTIONS_RUNTIME_URL"]);
Assert.Equal("token", environment["ACTIONS_RUNTIME_TOKEN"]);
Assert.False(environment.ContainsKey("ACTIONS_CACHE_MODE"));
Assert.False(environment.ContainsKey("ACTIONS_CACHE_SERVICE_V2"));
}
}

[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("read")]
[InlineData("none")]
public async Task ContainerRunAsync_ExportsCacheModeEnv_WhenVariableSet(string mode)
{
// Container actions only run on Linux; RunAsync throws on other platforms.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return;
}

using (TestHostContext hc = CreateTestContext())
{
var container = await RunContainerActionHandlerAsync(hc, new Dictionary<string, VariableValue>
{
{ "actions_cache_mode", mode }
});

Assert.True(container.ContainerEnvironmentVariables.TryGetValue("ACTIONS_CACHE_MODE", out var value));
Assert.Equal(mode, value);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task ContainerRunAsync_DoesNotExportCacheModeEnv_WhenVariableAbsent()
{
// Container actions only run on Linux; RunAsync throws on other platforms.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return;
}

using (TestHostContext hc = CreateTestContext())
{
var container = await RunContainerActionHandlerAsync(hc, new Dictionary<string, VariableValue>());

Assert.False(container.ContainerEnvironmentVariables.ContainsKey("ACTIONS_CACHE_MODE"));
}
}

private async Task<ContainerInfo> RunContainerActionHandlerAsync(TestHostContext hc, IDictionary<string, VariableValue> variables)
{
// Route through the container-hooks path so the handler skips docker build/run.
variables[Constants.Runner.Features.AllowRunnerContainerHooks] = "true";
Environment.SetEnvironmentVariable(Constants.Hooks.ContainerHooksPath, Path.Combine(hc.GetDirectory(WellKnownDirectory.Root), "hooks.js"));

var tempDirectory = hc.GetDirectory(WellKnownDirectory.Temp);
Directory.CreateDirectory(Path.Combine(tempDirectory, "_runner_file_commands"));
Directory.CreateDirectory(Path.Combine(tempDirectory, "_github_workflow"));
var workspace = Path.Combine(hc.GetDirectory(WellKnownDirectory.Work), "workspace");
Directory.CreateDirectory(workspace);

var serverVariables = new Variables(hc, variables);
var endpoints = new List<ServiceEndpoint>
{
new ServiceEndpoint()
{
Name = WellKnownServiceEndpointNames.SystemVssConnection,
Url = new Uri("https://pipelines.actions.githubusercontent.com"),
Authorization = new EndpointAuthorization()
{
Scheme = "Test",
Parameters = { { "AccessToken", "token" } }
}
}
};

_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
Endpoints = endpoints,
PrependPath = new List<string>(),
EnvironmentVariables = new Dictionary<string, string>()
});
_ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());
_ec.Setup(x => x.JobContext).Returns(new JobContext());
_ec.Setup(x => x.GetGitHubContext("workspace")).Returns(workspace);

ContainerInfo captured = null;
var hookManager = new Mock<IContainerHookManager>();
hookManager.Setup(x => x.RunContainerStepAsync(It.IsAny<IExecutionContext>(), It.IsAny<ContainerInfo>(), It.IsAny<string>()))
.Callback((IExecutionContext ec, ContainerInfo container, string dockerFile) => { captured = container; })
.Returns(Task.CompletedTask);
hc.SetSingleton(hookManager.Object);
hc.SetSingleton(new Mock<IActionManifestManagerWrapper>().Object);

var handler = new ContainerActionHandler();
handler.Initialize(hc);
handler.ExecutionContext = _ec.Object;
handler.Environment = new Dictionary<string, string>();
handler.Inputs = new Dictionary<string, string>();
handler.Action = new ContainerRegistryReference() { Image = "alpine:latest" };
handler.Data = new ContainerActionExecutionData() { Image = "docker://alpine:latest" };

await handler.RunAsync(ActionRunStage.Main);

return captured;
}

private async Task<Dictionary<string, string>> RunNodeScriptActionHandlerAsync(TestHostContext hc, IDictionary<string, VariableValue> variables)
{
var actionDirectory = Path.Combine(hc.GetDirectory(WellKnownDirectory.Work), Guid.NewGuid().ToString());
Directory.CreateDirectory(actionDirectory);
var scriptFile = "main.js";
File.WriteAllText(Path.Combine(actionDirectory, scriptFile), "// noop");

var serverVariables = new Variables(hc, variables);
var endpoints = new List<ServiceEndpoint>
{
new ServiceEndpoint()
{
Name = WellKnownServiceEndpointNames.SystemVssConnection,
Url = new Uri("https://pipelines.actions.githubusercontent.com"),
Authorization = new EndpointAuthorization()
{
Scheme = "Test",
Parameters = { { "AccessToken", "token" } }
}
}
};

_ec.Setup(x => x.Global).Returns(new GlobalContext()
{
Variables = serverVariables,
Endpoints = endpoints,
PrependPath = new List<string>(),
EnvironmentVariables = new Dictionary<string, string>()
});
_ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());
_ec.Setup(x => x.GetGitHubContext("workspace")).Returns(actionDirectory);
_ec.Setup(x => x.GetMatchers()).Returns(new List<IssueMatcherConfig>());
_ec.Setup(x => x.ForceCompleted).Returns(new TaskCompletionSource<int>().Task);
_ec.Setup(x => x.CancellationToken).Returns(CancellationToken.None);

var stepHost = new Mock<IStepHost>();
stepHost.Setup(x => x.DetermineNodeRuntimeVersion(It.IsAny<IExecutionContext>(), It.IsAny<string>())).ReturnsAsync("node20");
stepHost.Setup(x => x.ResolvePathForStepHost(It.IsAny<IExecutionContext>(), It.IsAny<string>())).Returns((IExecutionContext ec, string path) => path);
stepHost.Setup(x => x.ExecuteAsync(
It.IsAny<IExecutionContext>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<IDictionary<string, string>>(),
It.IsAny<bool>(),
It.IsAny<System.Text.Encoding>(),
It.IsAny<bool>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>())).ReturnsAsync(0);

var handler = new NodeScriptActionHandler();
handler.Initialize(hc);
handler.ExecutionContext = _ec.Object;
handler.StepHost = stepHost.Object;
handler.Environment = new Dictionary<string, string>();
handler.Inputs = new Dictionary<string, string>();
handler.RuntimeVariables = serverVariables;
handler.ActionDirectory = actionDirectory;
handler.Action = new RepositoryPathReference() { Name = "actions/checkout", Ref = "v2" };
handler.Data = new NodeJSActionExecutionData() { Script = scriptFile, NodeVersion = "node20" };

await handler.RunAsync(ActionRunStage.Main);

return handler.Environment;
}
}
}
48 changes: 48 additions & 0 deletions src/Test/L0/Worker/JobExtensionL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,54 @@ public async Task JobExtensionBuildPreStepsList()
}
}

[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("read")]
[InlineData("none")]
[InlineData("write")]
[InlineData("write-only")]
public async Task InitializeJob_LogsCacheMode_WhenVariableSet(string mode)
{
using (TestHostContext hc = CreateTestContext())
{
_jobEc.Global.Variables.Set("actions_cache_mode", mode);

var jobExtension = new JobExtension();
jobExtension.Initialize(hc);

_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);

_jobServerQueue.Verify(
x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(m => m.Contains($"Actions cache-mode: {mode}")), It.IsAny<long?>()),
Times.Once);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task InitializeJob_DoesNotLogCacheMode_WhenVariableAbsent()
{
using (TestHostContext hc = CreateTestContext())
{
var jobExtension = new JobExtension();
jobExtension.Initialize(hc);

_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);

_jobServerQueue.Verify(
x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(m => m.Contains("Actions cache-mode:")), It.IsAny<long?>()),
Times.Never);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
Expand Down
Loading