Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
26 changes: 26 additions & 0 deletions src/Runner.Worker/BackgroundStepContext.cs
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;
}
}
}
41 changes: 41 additions & 0 deletions src/Runner.Worker/CancelStepRunner.cs
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;
}
}
}
56 changes: 56 additions & 0 deletions src/Runner.Worker/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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")
{
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();
Expand Down Expand Up @@ -618,6 +642,8 @@ public string GetGitHubContext(string name)
}
}

public Dictionary<string, string> DeferredOutputs { get; set; }

public void SetOutput(string name, string value, out string reference)
{
ArgUtil.NotNullOrEmpty(name, nameof(name));
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
141 changes: 141 additions & 0 deletions src/Runner.Worker/JobExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,53 @@ public async Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipel
preJobSteps.Add(preStep);
}
}
else if (step.Type == Pipelines.StepType.Wait)
{
var waitStep = step as Pipelines.WaitStep;
Trace.Info($"Adding wait step for: {string.Join(", ", waitStep.WaitStepIds ?? System.Array.Empty<string>())}");
Trace.Info($"Wait step: DisplayNameToken={waitStep.DisplayNameToken?.GetType().Name ?? "null"}, DisplayName={step.DisplayName ?? "null"}, Name={step.Name ?? "null"}");
var waitStepName = (waitStep.DisplayNameToken as GitHub.DistributedTask.ObjectTemplating.Tokens.StringToken)?.Value
?? step.DisplayName ?? step.Name ?? "Wait for background steps";
Trace.Info($"Wait step resolved name: {waitStepName}");
var waitRunner = new WaitStepRunner
{
StepIds = waitStep.WaitStepIds,
DisplayName = waitStepName,
Condition = step.Condition,
StepId = step.Id,
StepName = step.Name,
};
// ExecutionContext created later in "Create execution context for job steps" loop
jobSteps.Add(waitRunner);
}
else if (step.Type == Pipelines.StepType.WaitAll)
{
Trace.Info("Adding wait-all step.");
var waitAllRunner = new WaitAllStepRunner
{
DisplayName = step.DisplayName ?? step.Name ?? "Wait for all background steps",
Condition = step.Condition,
StepId = step.Id,
StepName = step.Name,
};
// ExecutionContext created later in "Create execution context for job steps" loop
jobSteps.Add(waitAllRunner);
}
else if (step.Type == Pipelines.StepType.Cancel)
{
var cancelStep = step as Pipelines.CancelStep;
Trace.Info($"Adding cancel step for: {cancelStep.CancelStepId}");
var cancelRunner = new CancelStepRunner
{
CancelStepId = cancelStep.CancelStepId,
DisplayName = (cancelStep.DisplayNameToken as GitHub.DistributedTask.ObjectTemplating.Tokens.StringToken)?.Value ?? step.DisplayName ?? step.Name ?? "Cancel background step",
Condition = step.Condition,
StepId = step.Id,
StepName = step.Name,
};
// ExecutionContext created later in "Create execution context for job steps" loop
jobSteps.Add(cancelRunner);
}
}

if (message.Variables.TryGetValue("system.workflowFileFullPath", out VariableValue workflowFileFullPath))
Expand Down Expand Up @@ -400,14 +447,108 @@ public async Task<List<IStep>> InitializeJob(IExecutionContext jobContext, Pipel
}

// Create execution context for job steps
// Build mapping of logical step ID (ContextName) → external ID (timeline record GUID)
// so wait/cancel steps can reference background steps by external ID.
var contextNameToExternalId = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var hasBackgroundSteps = false;
var backgroundStepExternalIds = new List<string>();

// Track which background steps are explicitly covered by wait/wait-all/cancel
var coveredBackgroundIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
bool hasExplicitWaitAll = false;

foreach (var step in jobSteps)
{
if (step is IActionRunner actionStep)
{
ArgUtil.NotNull(actionStep, step.DisplayName);
intraActionStates.TryGetValue(actionStep.Action.Id, out var intraActionState);
actionStep.ExecutionContext = jobContext.CreateChild(actionStep.Action.Id, actionStep.DisplayName, actionStep.Action.Name, null, actionStep.Action.ContextName, ActionRunStage.Main, intraActionState);

// Store background step metadata on the timeline record for results service
if (actionStep.Action?.Background == true)
Comment thread
lokesh755 marked this conversation as resolved.
{
hasBackgroundSteps = true;
var externalId = actionStep.Action.Id.ToString("N");
contextNameToExternalId[actionStep.Action.ContextName] = externalId;
backgroundStepExternalIds.Add(externalId);
actionStep.ExecutionContext.SetTimelineRecordVariable("is_background", "true");
actionStep.ExecutionContext.SetTimelineRecordVariable("step_type", "action");
}
}
else if (step is WaitStepRunner waitRunner)
{
waitRunner.ExecutionContext = jobContext.CreateChild(
waitRunner.StepId, waitRunner.DisplayName, waitRunner.StepName,
null, waitRunner.StepName, ActionRunStage.Main);
waitRunner.ExecutionContext.SetTimelineRecordVariable("step_type", "wait");
if (waitRunner.StepIds != null && waitRunner.StepIds.Length > 0)
{
foreach (var id in waitRunner.StepIds)
{
coveredBackgroundIds.Add(id);
}
// Map logical step IDs to external GUIDs
var externalIds = waitRunner.StepIds
.Where(id => contextNameToExternalId.ContainsKey(id))
.Select(id => contextNameToExternalId[id])
.ToList();
if (externalIds.Count > 0)
{
waitRunner.ExecutionContext.SetTimelineRecordVariable("wait_step_ids", string.Join(",", externalIds));
}
}
Comment thread
lokesh755 marked this conversation as resolved.
}
else if (step is WaitAllStepRunner waitAllRunner)
{
hasExplicitWaitAll = true;
waitAllRunner.ExecutionContext = jobContext.CreateChild(
waitAllRunner.StepId, waitAllRunner.DisplayName, waitAllRunner.StepName,
null, waitAllRunner.StepName, ActionRunStage.Main);
waitAllRunner.ExecutionContext.SetTimelineRecordVariable("step_type", "wait-all");
if (backgroundStepExternalIds.Count > 0)
{
waitAllRunner.ExecutionContext.SetTimelineRecordVariable("wait_step_ids", string.Join(",", backgroundStepExternalIds));
}
}
else if (step is CancelStepRunner cancelRunner)
{
cancelRunner.ExecutionContext = jobContext.CreateChild(
cancelRunner.StepId, cancelRunner.DisplayName, cancelRunner.StepName,
null, cancelRunner.StepName, ActionRunStage.Main);
cancelRunner.ExecutionContext.SetTimelineRecordVariable("step_type", "cancel");
if (!string.IsNullOrEmpty(cancelRunner.CancelStepId))
{
coveredBackgroundIds.Add(cancelRunner.CancelStepId);
if (contextNameToExternalId.TryGetValue(cancelRunner.CancelStepId, out var cancelExternalId))
{
cancelRunner.ExecutionContext.SetTimelineRecordVariable("cancel_step_id", cancelExternalId);
}
}
}
}

// Add implicit wait-all only if there are uncovered background steps
var allBackgroundIds = contextNameToExternalId.Keys;
var hasUncoveredBackgroundSteps = !hasExplicitWaitAll && allBackgroundIds.Any(id => !coveredBackgroundIds.Contains(id));
if (hasBackgroundSteps && hasUncoveredBackgroundSteps)
{
var implicitWaitAll = new WaitAllStepRunner
{
DisplayName = "Wait for all background steps",
Condition = "always()",
StepId = Guid.NewGuid(),
StepName = "__implicit_wait_all",
};
implicitWaitAll.ExecutionContext = jobContext.CreateChild(
implicitWaitAll.StepId, implicitWaitAll.DisplayName, implicitWaitAll.StepName,
null, implicitWaitAll.StepName, ActionRunStage.Main);
implicitWaitAll.ExecutionContext.SetTimelineRecordVariable("step_type", "wait-all");
if (backgroundStepExternalIds.Count > 0)
{
implicitWaitAll.ExecutionContext.SetTimelineRecordVariable("wait_step_ids", string.Join(",", backgroundStepExternalIds));
}
jobSteps.Add(implicitWaitAll);
}

// Register custom image creation post-job step if the "snapshot" token is present in the message.
Expand Down
Loading
Loading