-
Notifications
You must be signed in to change notification settings - Fork 1.3k
support background steps #4416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
support background steps #4416
Changes from 8 commits
b3cb470
1002fd1
f74e5c6
4d32df6
9937faf
3520669
ce32f15
000b3bd
2fbefec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| using System; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace GitHub.Runner.Worker | ||
| { | ||
| /// <summary> | ||
| /// Tracks a background step's execution state. | ||
| /// </summary> | ||
| internal sealed class BackgroundStepContext | ||
| { | ||
| public string StepId { get; } | ||
| public IStep Step { get; } | ||
| public Task ExecutionTask { get; set; } | ||
| public CancellationTokenSource Cts { get; set; } | ||
| public GitHub.DistributedTask.WebApi.TaskResult? Result { get; set; } | ||
| public bool IsCompleted => ExecutionTask?.IsCompleted ?? false; | ||
| public string ExternalId => Step.ExecutionContext.Id.ToString("N"); | ||
|
|
||
| public BackgroundStepContext(string stepId, IStep step) | ||
| { | ||
| StepId = stepId; | ||
| Step = step; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| using System; | ||
| using System.Threading.Tasks; | ||
| using GitHub.DistributedTask.ObjectTemplating.Tokens; | ||
| using GitHub.DistributedTask.Pipelines.ContextData; | ||
|
|
||
| namespace GitHub.Runner.Worker | ||
| { | ||
| /// <summary> | ||
| /// A step that cancels a specific background step. | ||
| /// Execution is handled by StepsRunner, not by RunAsync. | ||
| /// </summary> | ||
| public sealed class CancelStepRunner : IStep | ||
| { | ||
| public string CancelStepId { get; set; } | ||
| public Guid StepId { get; set; } | ||
| public string StepName { get; set; } | ||
| public int RecordOrder { get; set; } | ||
| public string Condition { get; set; } | ||
| public TemplateToken ContinueOnError => null; | ||
| public string DisplayName { get; set; } | ||
| public IExecutionContext ExecutionContext { get; set; } | ||
| public TemplateToken Timeout => null; | ||
|
|
||
| public bool TryUpdateDisplayName(out bool updated) | ||
| { | ||
| updated = false; | ||
| return true; | ||
| } | ||
|
|
||
| public bool EvaluateDisplayName(DictionaryContextData contextData, IExecutionContext context, out bool updated) | ||
| { | ||
| updated = false; | ||
| return true; | ||
| } | ||
|
|
||
| public Task RunAsync() | ||
| { | ||
| return Task.CompletedTask; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,11 +100,17 @@ public interface IExecutionContext : IRunnerService | |
| void SetGitHubContext(string name, string value); | ||
| void SetOutput(string name, string value, out string reference); | ||
| void SetTimeout(TimeSpan? timeout); | ||
|
|
||
| // Background step output deferral | ||
| Dictionary<string, string> DeferredOutputs { get; set; } | ||
| void FlushDeferredOutputs(); | ||
|
|
||
| void AddIssue(Issue issue, ExecutionContextLogOptions logOptions); | ||
| void Progress(int percentage, string currentOperation = null); | ||
| void UpdateDetailTimelineRecord(TimelineRecord record); | ||
|
|
||
| void UpdateTimelineRecordDisplayName(string displayName); | ||
| void SetTimelineRecordVariable(string name, string value); | ||
|
|
||
| // matchers | ||
| void Add(OnMatcherChanged handler); | ||
|
|
@@ -511,6 +517,24 @@ public TaskResult Complete(TaskResult? result = null, string currentOperation = | |
| Annotations = new List<Annotation>() | ||
| }; | ||
|
|
||
| // Populate background step metadata from timeline record variables | ||
| if (_record.Variables.TryGetValue("is_background", out var bgVar) && bgVar.Value == "true") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are timeline record variables used internally to pass info around within the runner? I'm wondering if this information is available in a first class way internally (e.g. instance step instance type), rather than grabbing from a property bag here. |
||
| { | ||
| stepResult.IsBackground = true; | ||
| } | ||
| if (_record.Variables.TryGetValue("step_type", out var stVar) && !string.IsNullOrEmpty(stVar.Value)) | ||
| { | ||
| stepResult.StepType = stVar.Value; | ||
| } | ||
| if (_record.Variables.TryGetValue("wait_step_ids", out var wsVar) && !string.IsNullOrEmpty(wsVar.Value)) | ||
| { | ||
| stepResult.WaitStepIds = wsVar.Value.Split(','); | ||
| } | ||
| if (_record.Variables.TryGetValue("cancel_step_id", out var csVar) && !string.IsNullOrEmpty(csVar.Value)) | ||
| { | ||
| stepResult.CancelStepId = csVar.Value; | ||
| } | ||
|
|
||
| _record.Issues?.ForEach(issue => | ||
| { | ||
| var annotation = issue.ToAnnotation(); | ||
|
|
@@ -618,6 +642,8 @@ public string GetGitHubContext(string name) | |
| } | ||
| } | ||
|
|
||
| public Dictionary<string, string> DeferredOutputs { get; set; } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are all the other properties sorted above methods? |
||
|
|
||
| public void SetOutput(string name, string value, out string reference) | ||
| { | ||
| ArgUtil.NotNullOrEmpty(name, nameof(name)); | ||
|
|
@@ -629,11 +655,35 @@ public void SetOutput(string name, string value, out string reference) | |
| return; | ||
| } | ||
|
|
||
| // For background steps, buffer outputs instead of writing to StepsContext. | ||
| // Outputs are flushed to StepsContext when a wait/wait-all step completes. | ||
| if (DeferredOutputs != null) | ||
| { | ||
| DeferredOutputs[name] = value; | ||
| reference = System.Text.RegularExpressions.Regex.IsMatch(name, "^[a-zA-Z_][a-zA-Z0-9_]*$") | ||
| ? $"steps.{ContextName}.outputs.{name}" | ||
| : $"steps['{ContextName}']['outputs']['{name}']"; | ||
| return; | ||
| } | ||
|
|
||
| // todo: restrict multiline? | ||
|
|
||
| Global.StepsContext.SetOutput(ScopeName, ContextName, name, value, out reference); | ||
| } | ||
|
|
||
| public void FlushDeferredOutputs() | ||
| { | ||
| if (DeferredOutputs == null || DeferredOutputs.Count == 0) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| foreach (var kvp in DeferredOutputs) | ||
| { | ||
| Global.StepsContext.SetOutput(ScopeName, ContextName, kvp.Key, kvp.Value, out _); | ||
| } | ||
| } | ||
|
|
||
| public void SetTimeout(TimeSpan? timeout) | ||
| { | ||
| if (timeout != null) | ||
|
|
@@ -807,6 +857,12 @@ public void UpdateTimelineRecordDisplayName(string displayName) | |
| _jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record); | ||
| } | ||
|
|
||
| public void SetTimelineRecordVariable(string name, string value) | ||
| { | ||
| _record.Variables[name] = new VariableValue(value); | ||
| _jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record); | ||
| } | ||
|
|
||
| public void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token) | ||
| { | ||
| // Validation | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Properties are sorted before methods.
This interface is huge. I'm wondering if it would be good to organize all the new "Deferred" properties together as one visual block, and all the new methods "Flush" methods together as one visual block.