mirror of
https://github.com/actions/runner.git
synced 2026-01-23 13:01:14 +08:00
editing jobs
This commit is contained in:
444
src/Runner.Worker/Dap/StepCommands/StepFactory.cs
Normal file
444
src/Runner.Worker/Dap/StepCommands/StepFactory.cs
Normal file
@@ -0,0 +1,444 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap.StepCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for creating ActionStep and IActionRunner objects at runtime.
|
||||
/// Used by step commands to dynamically add steps during debug sessions.
|
||||
/// </summary>
|
||||
[ServiceLocator(Default = typeof(StepFactory))]
|
||||
public interface IStepFactory : IRunnerService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new run step (script step).
|
||||
/// </summary>
|
||||
/// <param name="script">The script to execute</param>
|
||||
/// <param name="name">Optional display name for the step</param>
|
||||
/// <param name="shell">Optional shell (bash, sh, pwsh, python, etc.)</param>
|
||||
/// <param name="workingDirectory">Optional working directory</param>
|
||||
/// <param name="env">Optional environment variables</param>
|
||||
/// <param name="condition">Optional condition expression (defaults to "success()")</param>
|
||||
/// <param name="continueOnError">Whether to continue on error (defaults to false)</param>
|
||||
/// <param name="timeoutMinutes">Optional timeout in minutes</param>
|
||||
/// <returns>A configured ActionStep with ScriptReference</returns>
|
||||
ActionStep CreateRunStep(
|
||||
string script,
|
||||
string name = null,
|
||||
string shell = null,
|
||||
string workingDirectory = null,
|
||||
Dictionary<string, string> env = null,
|
||||
string condition = null,
|
||||
bool continueOnError = false,
|
||||
int? timeoutMinutes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new uses step (action step).
|
||||
/// </summary>
|
||||
/// <param name="actionReference">The action reference (e.g., "actions/checkout@v4", "owner/repo@ref", "./local-action")</param>
|
||||
/// <param name="name">Optional display name for the step</param>
|
||||
/// <param name="with">Optional input parameters for the action</param>
|
||||
/// <param name="env">Optional environment variables</param>
|
||||
/// <param name="condition">Optional condition expression (defaults to "success()")</param>
|
||||
/// <param name="continueOnError">Whether to continue on error (defaults to false)</param>
|
||||
/// <param name="timeoutMinutes">Optional timeout in minutes</param>
|
||||
/// <returns>A configured ActionStep with RepositoryPathReference or ContainerRegistryReference</returns>
|
||||
ActionStep CreateUsesStep(
|
||||
string actionReference,
|
||||
string name = null,
|
||||
Dictionary<string, string> with = null,
|
||||
Dictionary<string, string> env = null,
|
||||
string condition = null,
|
||||
bool continueOnError = false,
|
||||
int? timeoutMinutes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an ActionStep in an IActionRunner for execution.
|
||||
/// </summary>
|
||||
/// <param name="step">The ActionStep to wrap</param>
|
||||
/// <param name="jobContext">The job execution context</param>
|
||||
/// <param name="stage">The execution stage (Main, Pre, or Post)</param>
|
||||
/// <returns>An IActionRunner ready for execution</returns>
|
||||
IActionRunner WrapInRunner(
|
||||
ActionStep step,
|
||||
IExecutionContext jobContext,
|
||||
ActionRunStage stage = ActionRunStage.Main);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsed components of an action reference string.
|
||||
/// </summary>
|
||||
public class ParsedActionReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of action reference.
|
||||
/// </summary>
|
||||
public ActionReferenceType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For GitHub actions: "owner/repo". For local: null. For docker: null.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For GitHub actions: the git ref (tag/branch/commit). For local/docker: null.
|
||||
/// </summary>
|
||||
public string Ref { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For actions in subdirectories: the path within the repo. For local: the full path.
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For docker actions: the image reference.
|
||||
/// </summary>
|
||||
public string Image { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of action references.
|
||||
/// </summary>
|
||||
public enum ActionReferenceType
|
||||
{
|
||||
/// <summary>GitHub repository action (e.g., "actions/checkout@v4")</summary>
|
||||
Repository,
|
||||
/// <summary>Local action (e.g., "./.github/actions/my-action")</summary>
|
||||
Local,
|
||||
/// <summary>Docker container action (e.g., "docker://alpine:latest")</summary>
|
||||
Docker
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating ActionStep and IActionRunner objects at runtime.
|
||||
/// </summary>
|
||||
public sealed class StepFactory : RunnerService, IStepFactory
|
||||
{
|
||||
// Constants for script step inputs (matching PipelineConstants.ScriptStepInputs)
|
||||
private const string ScriptInputKey = "script";
|
||||
private const string ShellInputKey = "shell";
|
||||
private const string WorkingDirectoryInputKey = "workingDirectory";
|
||||
|
||||
// Regex for parsing action references
|
||||
// Matches: owner/repo@ref, owner/repo/path@ref, owner/repo@ref/path (unusual but valid)
|
||||
private static readonly Regex ActionRefRegex = new Regex(
|
||||
@"^(?<name>[^/@]+/[^/@]+)(?:/(?<path>[^@]+))?@(?<ref>.+)$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ActionStep CreateRunStep(
|
||||
string script,
|
||||
string name = null,
|
||||
string shell = null,
|
||||
string workingDirectory = null,
|
||||
Dictionary<string, string> env = null,
|
||||
string condition = null,
|
||||
bool continueOnError = false,
|
||||
int? timeoutMinutes = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(script))
|
||||
{
|
||||
throw new ArgumentException("Script cannot be null or empty", nameof(script));
|
||||
}
|
||||
|
||||
var stepId = Guid.NewGuid();
|
||||
var step = new ActionStep
|
||||
{
|
||||
Id = stepId,
|
||||
Name = $"_dynamic_{stepId:N}",
|
||||
DisplayName = name ?? "Run script",
|
||||
Reference = new ScriptReference(),
|
||||
Condition = condition ?? "success()",
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
// Build Inputs mapping with script, shell, working-directory
|
||||
step.Inputs = CreateRunInputs(script, shell, workingDirectory);
|
||||
|
||||
// Build Environment mapping
|
||||
if (env?.Count > 0)
|
||||
{
|
||||
step.Environment = CreateEnvToken(env);
|
||||
}
|
||||
|
||||
// Set continue-on-error
|
||||
if (continueOnError)
|
||||
{
|
||||
step.ContinueOnError = new BooleanToken(null, null, null, true);
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
if (timeoutMinutes.HasValue)
|
||||
{
|
||||
step.TimeoutInMinutes = new NumberToken(null, null, null, timeoutMinutes.Value);
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ActionStep CreateUsesStep(
|
||||
string actionReference,
|
||||
string name = null,
|
||||
Dictionary<string, string> with = null,
|
||||
Dictionary<string, string> env = null,
|
||||
string condition = null,
|
||||
bool continueOnError = false,
|
||||
int? timeoutMinutes = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(actionReference))
|
||||
{
|
||||
throw new ArgumentException("Action reference cannot be null or empty", nameof(actionReference));
|
||||
}
|
||||
|
||||
var parsed = ParseActionReference(actionReference);
|
||||
var stepId = Guid.NewGuid();
|
||||
|
||||
var step = new ActionStep
|
||||
{
|
||||
Id = stepId,
|
||||
Name = $"_dynamic_{stepId:N}",
|
||||
DisplayName = name ?? actionReference,
|
||||
Condition = condition ?? "success()",
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
// Set reference based on action type
|
||||
switch (parsed.Type)
|
||||
{
|
||||
case ActionReferenceType.Repository:
|
||||
step.Reference = new RepositoryPathReference
|
||||
{
|
||||
Name = parsed.Name,
|
||||
Ref = parsed.Ref,
|
||||
Path = parsed.Path,
|
||||
RepositoryType = "GitHub"
|
||||
};
|
||||
break;
|
||||
|
||||
case ActionReferenceType.Local:
|
||||
step.Reference = new RepositoryPathReference
|
||||
{
|
||||
RepositoryType = PipelineConstants.SelfAlias,
|
||||
Path = parsed.Path
|
||||
};
|
||||
break;
|
||||
|
||||
case ActionReferenceType.Docker:
|
||||
step.Reference = new ContainerRegistryReference
|
||||
{
|
||||
Image = parsed.Image
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
// Build with inputs
|
||||
if (with?.Count > 0)
|
||||
{
|
||||
step.Inputs = CreateWithInputs(with);
|
||||
}
|
||||
|
||||
// Build Environment mapping
|
||||
if (env?.Count > 0)
|
||||
{
|
||||
step.Environment = CreateEnvToken(env);
|
||||
}
|
||||
|
||||
// Set continue-on-error
|
||||
if (continueOnError)
|
||||
{
|
||||
step.ContinueOnError = new BooleanToken(null, null, null, true);
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
if (timeoutMinutes.HasValue)
|
||||
{
|
||||
step.TimeoutInMinutes = new NumberToken(null, null, null, timeoutMinutes.Value);
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IActionRunner WrapInRunner(
|
||||
ActionStep step,
|
||||
IExecutionContext jobContext,
|
||||
ActionRunStage stage = ActionRunStage.Main)
|
||||
{
|
||||
if (step == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(step));
|
||||
}
|
||||
if (jobContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(jobContext));
|
||||
}
|
||||
|
||||
var runner = HostContext.CreateService<IActionRunner>();
|
||||
runner.Action = step;
|
||||
runner.Stage = stage;
|
||||
runner.Condition = step.Condition;
|
||||
|
||||
// Create a child execution context for this step
|
||||
// The child context gets its own scope for outputs, logging, etc.
|
||||
// Following the pattern from JobExtension.cs line ~401
|
||||
runner.ExecutionContext = jobContext.CreateChild(
|
||||
recordId: step.Id,
|
||||
displayName: step.DisplayName,
|
||||
refName: step.Name,
|
||||
scopeName: null,
|
||||
contextName: step.ContextName,
|
||||
stage: stage
|
||||
);
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Parses an action reference string into its components.
|
||||
/// </summary>
|
||||
/// <param name="actionReference">The action reference (e.g., "actions/checkout@v4")</param>
|
||||
/// <returns>Parsed action reference components</returns>
|
||||
public static ParsedActionReference ParseActionReference(string actionReference)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actionReference))
|
||||
{
|
||||
throw new ArgumentException("Action reference cannot be null or empty", nameof(actionReference));
|
||||
}
|
||||
|
||||
var trimmed = actionReference.Trim();
|
||||
|
||||
// Check for docker action: docker://image:tag
|
||||
if (trimmed.StartsWith("docker://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new ParsedActionReference
|
||||
{
|
||||
Type = ActionReferenceType.Docker,
|
||||
Image = trimmed.Substring("docker://".Length)
|
||||
};
|
||||
}
|
||||
|
||||
// Check for local action: ./ or ../ prefix
|
||||
if (trimmed.StartsWith("./") || trimmed.StartsWith("../"))
|
||||
{
|
||||
return new ParsedActionReference
|
||||
{
|
||||
Type = ActionReferenceType.Local,
|
||||
Path = trimmed
|
||||
};
|
||||
}
|
||||
|
||||
// Parse as GitHub repository action: owner/repo@ref or owner/repo/path@ref
|
||||
var match = ActionRefRegex.Match(trimmed);
|
||||
if (match.Success)
|
||||
{
|
||||
var result = new ParsedActionReference
|
||||
{
|
||||
Type = ActionReferenceType.Repository,
|
||||
Name = match.Groups["name"].Value,
|
||||
Ref = match.Groups["ref"].Value
|
||||
};
|
||||
|
||||
if (match.Groups["path"].Success && !string.IsNullOrEmpty(match.Groups["path"].Value))
|
||||
{
|
||||
result.Path = match.Groups["path"].Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// If no @ sign, assume it's a local action path
|
||||
if (!trimmed.Contains("@"))
|
||||
{
|
||||
return new ParsedActionReference
|
||||
{
|
||||
Type = ActionReferenceType.Local,
|
||||
Path = trimmed
|
||||
};
|
||||
}
|
||||
|
||||
// Invalid format
|
||||
throw new StepCommandException(
|
||||
StepCommandErrors.ParseError,
|
||||
$"Invalid action reference format: '{actionReference}'. Expected: 'owner/repo@ref', './local-path', or 'docker://image:tag'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a MappingToken for run step inputs (script, shell, working-directory).
|
||||
/// </summary>
|
||||
private MappingToken CreateRunInputs(string script, string shell, string workingDirectory)
|
||||
{
|
||||
var inputs = new MappingToken(null, null, null);
|
||||
|
||||
// Script is always required
|
||||
inputs.Add(
|
||||
new StringToken(null, null, null, ScriptInputKey),
|
||||
new StringToken(null, null, null, script)
|
||||
);
|
||||
|
||||
// Shell is optional
|
||||
if (!string.IsNullOrEmpty(shell))
|
||||
{
|
||||
inputs.Add(
|
||||
new StringToken(null, null, null, ShellInputKey),
|
||||
new StringToken(null, null, null, shell)
|
||||
);
|
||||
}
|
||||
|
||||
// Working directory is optional
|
||||
if (!string.IsNullOrEmpty(workingDirectory))
|
||||
{
|
||||
inputs.Add(
|
||||
new StringToken(null, null, null, WorkingDirectoryInputKey),
|
||||
new StringToken(null, null, null, workingDirectory)
|
||||
);
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a MappingToken for action "with" inputs.
|
||||
/// </summary>
|
||||
private MappingToken CreateWithInputs(Dictionary<string, string> with)
|
||||
{
|
||||
var inputs = new MappingToken(null, null, null);
|
||||
|
||||
foreach (var kvp in with)
|
||||
{
|
||||
inputs.Add(
|
||||
new StringToken(null, null, null, kvp.Key),
|
||||
new StringToken(null, null, null, kvp.Value ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a MappingToken for environment variables.
|
||||
/// </summary>
|
||||
private MappingToken CreateEnvToken(Dictionary<string, string> env)
|
||||
{
|
||||
var envMapping = new MappingToken(null, null, null);
|
||||
|
||||
foreach (var kvp in env)
|
||||
{
|
||||
envMapping.Add(
|
||||
new StringToken(null, null, null, kvp.Key),
|
||||
new StringToken(null, null, null, kvp.Value ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
return envMapping;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user