diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index c010fa908..c420ddfc8 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -392,6 +392,18 @@ namespace GitHub.Runner.Worker } } + // Register custom image creation post-job step if the "snapshot" token is present in the message. + var snapshotRequest = templateEvaluator.EvaluateJobSnapshotRequest(message.Snapshot, jobContext.ExpressionValues, jobContext.ExpressionFunctions); + if (snapshotRequest != null) + { + var snapshotOperationProvider = HostContext.GetService(); + jobContext.RegisterPostJobStep(new JobExtensionRunner( + runAsync: (executionContext, _) => snapshotOperationProvider.CreateSnapshotRequestAsync(executionContext, snapshotRequest), + condition: $"{PipelineTemplateConstants.Success}()", + displayName: $"Create custom image", + data: null)); + } + // Register Job Completed hook if the variable is set var completedHookPath = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_HOOK_JOB_COMPLETED"); if (!string.IsNullOrEmpty(completedHookPath)) diff --git a/src/Runner.Worker/SnapshotOperationProvider.cs b/src/Runner.Worker/SnapshotOperationProvider.cs new file mode 100644 index 000000000..73630d498 --- /dev/null +++ b/src/Runner.Worker/SnapshotOperationProvider.cs @@ -0,0 +1,32 @@ +#nullable enable +using System.IO; +using System.Threading.Tasks; +using GitHub.DistributedTask.Pipelines; +using GitHub.Runner.Common; +using GitHub.Runner.Sdk; +namespace GitHub.Runner.Worker; + +[ServiceLocator(Default = typeof(SnapshotOperationProvider))] +public interface ISnapshotOperationProvider : IRunnerService +{ + Task CreateSnapshotRequestAsync(IExecutionContext executionContext, Snapshot snapshotRequest); +} + +public class SnapshotOperationProvider : RunnerService, ISnapshotOperationProvider +{ + public Task CreateSnapshotRequestAsync(IExecutionContext executionContext, Snapshot snapshotRequest) + { + var snapshotRequestFilePath = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), ".snapshot", "request.json"); + var snapshotRequestDirectoryPath = Path.GetDirectoryName(snapshotRequestFilePath); + if (snapshotRequestDirectoryPath != null) + { + Directory.CreateDirectory(snapshotRequestDirectoryPath); + } + + IOUtil.SaveObject(snapshotRequest, snapshotRequestFilePath); + executionContext.Output($"Request written to: {snapshotRequestFilePath}"); + executionContext.Output("This request will be processed after the job completes. You will not receive any feedback on the snapshot process within the workflow logs of this job."); + executionContext.Output("If the snapshot process is successful, you should see a new image with the requested name in the list of available custom images when creating a new GitHub-hosted Runner."); + return Task.CompletedTask; + } +} diff --git a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs index 3f93e75e5..070d86ee2 100644 --- a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs +++ b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs @@ -43,6 +43,7 @@ namespace GitHub.DistributedTask.Pipelines TemplateToken jobOutputs, IList defaults, ActionsEnvironmentReference actionsEnvironment, + TemplateToken snapshot, String messageType = JobRequestMessageTypes.PipelineAgentJobRequest) { this.MessageType = messageType; @@ -57,6 +58,7 @@ namespace GitHub.DistributedTask.Pipelines this.Workspace = workspaceOptions; this.JobOutputs = jobOutputs; this.ActionsEnvironment = actionsEnvironment; + this.Snapshot = snapshot; m_variables = new Dictionary(variables, StringComparer.OrdinalIgnoreCase); m_maskHints = new List(maskHints); m_steps = new List(steps); @@ -237,6 +239,13 @@ namespace GitHub.DistributedTask.Pipelines set; } + [DataMember(EmitDefaultValue = false)] + public TemplateToken Snapshot + { + get; + set; + } + /// /// Gets the collection of variables associated with the current context. /// diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs index e9fb75dfa..a7e90fce3 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs @@ -29,6 +29,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating public const String Id = "id"; public const String If = "if"; public const String Image = "image"; + public const String ImageName = "image-name"; public const String Include = "include"; public const String Inputs = "inputs"; public const String Job = "job"; @@ -60,6 +61,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating public const String Services = "services"; public const String Shell = "shell"; public const String Skipped = "skipped"; + public const String Snapshot = "snapshot"; public const String StepEnv = "step-env"; public const String StepIfResult = "step-if-result"; public const String StepWith = "step-with"; diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs index 506a7d268..9d2c0bdca 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs @@ -346,6 +346,39 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating return result; } + internal static Snapshot ConvertToJobSnapshotRequest(TemplateContext context, TemplateToken token) + { + string imageName = null; + if (token is StringToken snapshotStringLiteral) + { + imageName = snapshotStringLiteral.Value; + } + else + { + var snapshotMapping = token.AssertMapping($"{PipelineTemplateConstants.Snapshot}"); + foreach (var snapshotPropertyPair in snapshotMapping) + { + var propertyName = snapshotPropertyPair.Key.AssertString($"{PipelineTemplateConstants.Snapshot} key"); + switch (propertyName.Value) + { + case PipelineTemplateConstants.ImageName: + imageName = snapshotPropertyPair.Value.AssertString($"{PipelineTemplateConstants.Snapshot} {propertyName}").Value; + break; + default: + propertyName.AssertUnexpectedValue($"{PipelineTemplateConstants.Snapshot} key"); + break; + } + } + } + + if (String.IsNullOrEmpty(imageName)) + { + return null; + } + + return new Snapshot(imageName); + } + private static ActionStep ConvertToStep( TemplateContext context, TemplateToken stepsItem, diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs index 331b10246..e5fbd5d28 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs @@ -370,6 +370,32 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating return result; } + public Snapshot EvaluateJobSnapshotRequest(TemplateToken token, + DictionaryContextData contextData, + IList expressionFunctions) + { + var result = default(Snapshot); + + if (token != null && token.Type != TokenType.Null) + { + var context = CreateContext(contextData, expressionFunctions); + try + { + token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Snapshot, token, 0, null, omitHeader: true); + context.Errors.Check(); + result = PipelineTemplateConverter.ConvertToJobSnapshotRequest(context, token); + } + catch (Exception ex) when (!(ex is TemplateValidationException)) + { + context.Errors.Add(ex); + } + + context.Errors.Check(); + } + + return result; + } + private TemplateContext CreateContext( DictionaryContextData contextData, IList expressionFunctions, diff --git a/src/Sdk/DTPipelines/Pipelines/Snapshot.cs b/src/Sdk/DTPipelines/Pipelines/Snapshot.cs new file mode 100644 index 000000000..60f8da04f --- /dev/null +++ b/src/Sdk/DTPipelines/Pipelines/Snapshot.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.Serialization; + +namespace GitHub.DistributedTask.Pipelines +{ + [DataContract] + public class Snapshot + { + public Snapshot(string imageName) + { + ImageName = imageName; + } + + [DataMember(EmitDefaultValue = false)] + public String ImageName { get; set; } + } +} diff --git a/src/Sdk/DTPipelines/workflow-v1.0.json b/src/Sdk/DTPipelines/workflow-v1.0.json index c1453f4e6..a3837edff 100644 --- a/src/Sdk/DTPipelines/workflow-v1.0.json +++ b/src/Sdk/DTPipelines/workflow-v1.0.json @@ -71,7 +71,8 @@ "env": "job-env", "outputs": "job-outputs", "defaults": "job-defaults", - "steps": "steps" + "steps": "steps", + "snapshot": "snapshot" } } }, @@ -155,6 +156,24 @@ } }, + "snapshot": { + "one-of": [ + "non-empty-string", + "snapshot-mapping" + ] + }, + + "snapshot-mapping": { + "mapping": { + "properties": { + "image-name": { + "type": "non-empty-string", + "required": true + } + } + } + }, + "runs-on": { "context": [ "github", diff --git a/src/Test/L0/Listener/JobDispatcherL0.cs b/src/Test/L0/Listener/JobDispatcherL0.cs index 9057e6bbf..4d3f258c8 100644 --- a/src/Test/L0/Listener/JobDispatcherL0.cs +++ b/src/Test/L0/Listener/JobDispatcherL0.cs @@ -41,7 +41,7 @@ namespace GitHub.Runner.Common.Tests.Listener TaskOrchestrationPlanReference plan = new(); TimelineReference timeline = null; Guid jobId = Guid.NewGuid(); - var result = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "someJob", "someJob", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var result = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "someJob", "someJob", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); result.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData(); return result; } @@ -806,7 +806,8 @@ namespace GitHub.Runner.Common.Tests.Listener }, null, new List(), - new ActionsEnvironmentReference("env") + new ActionsEnvironmentReference("env"), + null ); return message; } diff --git a/src/Test/L0/Listener/RunnerL0.cs b/src/Test/L0/Listener/RunnerL0.cs index 47df4de25..9c57f2adc 100644 --- a/src/Test/L0/Listener/RunnerL0.cs +++ b/src/Test/L0/Listener/RunnerL0.cs @@ -42,7 +42,7 @@ namespace GitHub.Runner.Common.Tests.Listener TaskOrchestrationPlanReference plan = new(); TimelineReference timeline = null; Guid jobId = Guid.NewGuid(); - return new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + return new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); } private JobCancelMessage CreateJobCancelMessage() diff --git a/src/Test/L0/Worker/ActionCommandManagerL0.cs b/src/Test/L0/Worker/ActionCommandManagerL0.cs index 693c6f025..3a1f8f70f 100644 --- a/src/Test/L0/Worker/ActionCommandManagerL0.cs +++ b/src/Test/L0/Worker/ActionCommandManagerL0.cs @@ -232,7 +232,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, diff --git a/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs b/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs index 19f956fa8..185f44b38 100644 --- a/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs +++ b/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs @@ -193,7 +193,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "Summary Job"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, diff --git a/src/Test/L0/Worker/ExecutionContextL0.cs b/src/Test/L0/Worker/ExecutionContextL0.cs index 91068d300..08abcd095 100644 --- a/src/Test/L0/Worker/ExecutionContextL0.cs +++ b/src/Test/L0/Worker/ExecutionContextL0.cs @@ -29,7 +29,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -106,7 +106,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -162,7 +162,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -216,7 +216,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -271,7 +271,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -322,7 +322,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -373,7 +373,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -471,7 +471,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -555,7 +555,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -610,7 +610,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -653,7 +653,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -717,7 +717,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -781,7 +781,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -969,7 +969,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -1014,7 +1014,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new TimelineReference(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -1057,7 +1057,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new TimelineReference(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index 3f5e074a8..d66ded663 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using GitHub.DistributedTask.ObjectTemplating.Tokens; +using GitHub.DistributedTask.Pipelines.ObjectTemplating; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Worker; using Moq; @@ -25,6 +27,9 @@ namespace GitHub.Runner.Common.Tests.Worker private Mock _containerProvider; private Mock _diagnosticLogManager; private Mock _jobHookProvider; + private Mock _snapshotOperationProvider; + + private Pipelines.Snapshot _requestedSnapshot; private CancellationTokenSource _tokenSource; private TestHostContext CreateTestContext([CallerMemberName] String testName = "") @@ -41,7 +46,16 @@ namespace GitHub.Runner.Common.Tests.Worker _directoryManager.Setup(x => x.PrepareDirectory(It.IsAny(), It.IsAny())) .Returns(new TrackingConfig() { PipelineDirectory = "runner", WorkspaceDirectory = "runner/runner" }); _jobHookProvider = new Mock(); + _snapshotOperationProvider = new Mock(); + _requestedSnapshot = null; + _snapshotOperationProvider + .Setup(p => p.CreateSnapshotRequestAsync(It.IsAny(), It.IsAny())) + .Returns((IExecutionContext _, object data) => + { + _requestedSnapshot = data as Pipelines.Snapshot; + return Task.CompletedTask; + }); IActionRunner step1 = new ActionRunner(); IActionRunner step2 = new ActionRunner(); IActionRunner step3 = new ActionRunner(); @@ -100,7 +114,7 @@ namespace GitHub.Runner.Common.Tests.Worker }; Guid jobId = Guid.NewGuid(); - _message = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), steps, null, null, null, null); + _message = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), steps, null, null, null, null, null); GitHubContext github = new(); github["repository"] = new Pipelines.ContextData.StringContextData("actions/runner"); github["secret_source"] = new Pipelines.ContextData.StringContextData("Actions"); @@ -125,6 +139,7 @@ namespace GitHub.Runner.Common.Tests.Worker hc.SetSingleton(_directoryManager.Object); hc.SetSingleton(_diagnosticLogManager.Object); hc.SetSingleton(_jobHookProvider.Object); + hc.SetSingleton(_snapshotOperationProvider.Object); hc.EnqueueInstance(_logger.Object); // JobExecutionContext hc.EnqueueInstance(_logger.Object); // job start hook hc.EnqueueInstance(_logger.Object); // Initial Job @@ -443,5 +458,80 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(0, _jobEc.PostJobSteps.Count); } } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task EnsureNoSnapshotPostJobStep() + { + using (TestHostContext hc = CreateTestContext()) + { + var jobExtension = new JobExtension(); + jobExtension.Initialize(hc); + + _actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns(Task.FromResult(new PrepareResult(new List(), new Dictionary()))); + + _message.Snapshot = null; + await jobExtension.InitializeJob(_jobEc, _message); + + var postJobSteps = _jobEc.PostJobSteps; + Assert.Equal(0, postJobSteps.Count); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public Task EnsureSnapshotPostJobStepForStringToken() + { + var snapshot = new Pipelines.Snapshot("TestImageNameFromStringToken"); + var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName); + return EnsureSnapshotPostJobStepForToken(imageNameValueStringToken, snapshot); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public Task EnsureSnapshotPostJobStepForMappingToken() + { + var snapshot = new Pipelines.Snapshot("TestImageNameFromMappingToken"); + var imageNameValueStringToken = new StringToken(null, null, null, snapshot.ImageName); + var mappingToken = new MappingToken(null, null, null) + { + { new StringToken(null,null,null, PipelineTemplateConstants.ImageName), imageNameValueStringToken } + }; + + return EnsureSnapshotPostJobStepForToken(mappingToken, snapshot); + } + + private async Task EnsureSnapshotPostJobStepForToken(TemplateToken snapshotToken, Pipelines.Snapshot expectedSnapshot) + { + using (TestHostContext hc = CreateTestContext()) + { + var jobExtension = new JobExtension(); + jobExtension.Initialize(hc); + + _actionManager.Setup(x => x.PrepareActionsAsync(It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns(Task.FromResult(new PrepareResult(new List(), new Dictionary()))); + + _message.Snapshot = snapshotToken; + + await jobExtension.InitializeJob(_jobEc, _message); + + var postJobSteps = _jobEc.PostJobSteps; + + Assert.Equal(1, postJobSteps.Count); + var snapshotStep = postJobSteps.First(); + Assert.Equal("Create custom image", snapshotStep.DisplayName); + Assert.Equal($"{PipelineTemplateConstants.Success}()", snapshotStep.Condition); + + // Run the mock snapshot step, so we can verify it was executed with the expected snapshot object. + await snapshotStep.RunAsync(); + + Assert.NotNull(_requestedSnapshot); + Assert.Equal(expectedSnapshot.ImageName, _requestedSnapshot.ImageName); + } + } } } diff --git a/src/Test/L0/Worker/JobRunnerL0.cs b/src/Test/L0/Worker/JobRunnerL0.cs index d4aaf809c..e8011b9b0 100644 --- a/src/Test/L0/Worker/JobRunnerL0.cs +++ b/src/Test/L0/Worker/JobRunnerL0.cs @@ -101,6 +101,7 @@ namespace GitHub.Runner.Common.Tests.Worker testName, testName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, new ActionsEnvironmentReference("staging"), + null, messageType: messageType); message.Variables[Constants.Variables.System.Culture] = "en-US"; message.Resources.Endpoints.Add(new ServiceEndpoint() diff --git a/src/Test/L0/Worker/SnapshotOperationProviderL0.cs b/src/Test/L0/Worker/SnapshotOperationProviderL0.cs new file mode 100644 index 000000000..4f747ae8e --- /dev/null +++ b/src/Test/L0/Worker/SnapshotOperationProviderL0.cs @@ -0,0 +1,78 @@ +#nullable enable +using System; +using System.IO; +using System.Runtime.CompilerServices; +using GitHub.DistributedTask.Pipelines; +using GitHub.Runner.Sdk; +using GitHub.Runner.Worker; +using Moq; +using Xunit; + +namespace GitHub.Runner.Common.Tests.Worker; + +public class SnapshotOperationProviderL0 +{ + private Mock? _ec; + private SnapshotOperationProvider? _snapshotOperationProvider; + private string? _snapshotRequestFilePath; + private string? _snapshotRequestDirectoryPath; + + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void CreateSnapshotRequestAsync(bool shouldSnapshotDirectoryAlreadyExist) + { + using (TestHostContext testHostContext = CreateTestHostContext()) + { + //Arrange + Setup(testHostContext, shouldSnapshotDirectoryAlreadyExist); + var expectedSnapshot = new Snapshot(Guid.NewGuid().ToString()); + + //Act + await _snapshotOperationProvider!.CreateSnapshotRequestAsync(_ec!.Object, expectedSnapshot); + + //Assert + var actualSnapshot = IOUtil.LoadObject(_snapshotRequestFilePath); + Assert.NotNull(actualSnapshot); + Assert.Equal(expectedSnapshot.ImageName, actualSnapshot!.ImageName); + _ec.Verify(ec => ec.Write(null, $"Request written to: {_snapshotRequestFilePath}"), Times.Once); + _ec.Verify(ec => ec.Write(null, "This request will be processed after the job completes. You will not receive any feedback on the snapshot process within the workflow logs of this job."), Times.Once); + _ec.Verify(ec => ec.Write(null, "If the snapshot process is successful, you should see a new image with the requested name in the list of available custom images when creating a new GitHub-hosted Runner."), Times.Once); + _ec.VerifyNoOtherCalls(); + } + } + + private void Setup(IHostContext hostContext, bool shouldSnapshotDirectoryAlreadyExist) + { + _ec = new Mock(); + _snapshotOperationProvider = new SnapshotOperationProvider(); + _snapshotOperationProvider.Initialize(hostContext); + _snapshotRequestFilePath = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Root), ".snapshot", "request.json"); + _snapshotRequestDirectoryPath = Path.GetDirectoryName(_snapshotRequestFilePath); + + if (_snapshotRequestDirectoryPath != null) + { + // Clean up any existing the snapshot directory and its contents before starting the test. + if (Directory.Exists(_snapshotRequestDirectoryPath)) + { + Directory.Delete(_snapshotRequestDirectoryPath, true); + } + + if (shouldSnapshotDirectoryAlreadyExist) + { + // Create a fresh snapshot directory if it's required for the test. + Directory.CreateDirectory(_snapshotRequestDirectoryPath); + } + } + } + + private TestHostContext CreateTestHostContext([CallerMemberName] string testName = "") + { + var testHostContext = new TestHostContext(this, testName); + _ec = new Mock(); + _ec.Object.Initialize(testHostContext); + return testHostContext; + } +} diff --git a/src/Test/L0/Worker/WorkerL0.cs b/src/Test/L0/Worker/WorkerL0.cs index fe1f9c02c..defcc9814 100644 --- a/src/Test/L0/Worker/WorkerL0.cs +++ b/src/Test/L0/Worker/WorkerL0.cs @@ -67,7 +67,7 @@ namespace GitHub.Runner.Common.Tests.Worker new Pipelines.ContextData.DictionaryContextData() }, }; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, JobId, jobName, jobName, new StringToken(null, null, null, "ubuntu"), sidecarContainers, null, variables, new List(), resources, context, null, actions, null, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, JobId, jobName, jobName, new StringToken(null, null, null, "ubuntu"), sidecarContainers, null, variables, new List(), resources, context, null, actions, null, null, null, null, null); return jobRequest; }