mirror of
https://github.com/actions/runner.git
synced 2026-01-23 04:51:23 +08:00
Add step command refinements: --here, --id, and help commands
- Add --here position option to insert steps before the current step - Add --id option to specify custom step IDs for expression references - Add --help flag support for all step commands with detailed usage info - Update browser extension UI with ID field and improved position dropdown
This commit is contained in:
853
.opencode/plans/dap-step-commands-refinements.md
Normal file
853
.opencode/plans/dap-step-commands-refinements.md
Normal file
@@ -0,0 +1,853 @@
|
|||||||
|
# Step Commands Refinements: --here, --id, and Help
|
||||||
|
|
||||||
|
**Status:** Draft
|
||||||
|
**Author:** GitHub Actions Team
|
||||||
|
**Date:** January 2026
|
||||||
|
**Prerequisites:** dap-step-manipulation.md (completed)
|
||||||
|
|
||||||
|
## Progress Checklist
|
||||||
|
|
||||||
|
- [x] **Chunk 1:** `--here` Position Option
|
||||||
|
- [x] **Chunk 2:** `--id` Option for Step Identification
|
||||||
|
- [x] **Chunk 3:** Help Commands (`--help`)
|
||||||
|
- [x] **Chunk 4:** Browser Extension UI Updates
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This plan addresses three refinements to the step manipulation commands based on user feedback:
|
||||||
|
|
||||||
|
1. **`--here` position option**: Insert a step before the current step (the one you're paused at), so it runs immediately when stepping forward
|
||||||
|
2. **`--id` option**: Allow users to specify a custom step ID for later reference (e.g., `steps.<id>.outputs`)
|
||||||
|
3. **Help commands**: Add `--help` flag support to all step commands for discoverability
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
### Issue 1: "First pending position" inserts in the wrong place
|
||||||
|
|
||||||
|
When paused before a step (e.g., checkout at position 1), using `--first` inserts the new step *after* the current step, not before it:
|
||||||
|
|
||||||
|
```
|
||||||
|
Before (paused at step 1):
|
||||||
|
▶ 1. Checkout
|
||||||
|
|
||||||
|
After "steps add run 'echo hello' --first":
|
||||||
|
▶ 1. Checkout
|
||||||
|
2. hello [ADDED] <-- Wrong! Should be before Checkout
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root cause:** `PositionType.First` returns index 0 of the `JobSteps` queue, which contains steps *after* the current step. The current step is held separately in `_currentStep`.
|
||||||
|
|
||||||
|
**Expected behavior:** User wants to insert a step that will run immediately when they continue, i.e., before the current step.
|
||||||
|
|
||||||
|
### Issue 2: No way to specify step ID
|
||||||
|
|
||||||
|
Dynamically added steps get auto-generated IDs like `_dynamic_<guid>`, making them impossible to reference in expressions like `steps.<id>.outputs.foo`.
|
||||||
|
|
||||||
|
### Issue 3: Command options are hard to remember
|
||||||
|
|
||||||
|
With growing options (`--name`, `--shell`, `--after`, `--before`, `--at`, `--first`, `--last`, etc.), users need a way to quickly see available options without consulting documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: `--here` Position Option
|
||||||
|
|
||||||
|
**Goal:** Add a new position option that inserts a step before the current step (the one paused at a breakpoint).
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
| Flag | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `--here` | Insert before the current step, so it becomes the next step to run |
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Only valid when paused at a breakpoint
|
||||||
|
- Returns error if not paused: "Can only use --here when paused at a breakpoint"
|
||||||
|
- Inserts the new step such that it will execute immediately when the user continues/steps forward
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
Before (paused at step 1):
|
||||||
|
▶ 1. Checkout
|
||||||
|
2. Build
|
||||||
|
3. Test
|
||||||
|
|
||||||
|
After "steps add run 'echo hello' --here":
|
||||||
|
▶ 1. hello [ADDED] <-- New step runs next
|
||||||
|
2. Checkout
|
||||||
|
3. Build
|
||||||
|
4. Test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `StepCommandParser.cs` | Add `Here` to `PositionType` enum; add `StepPosition.Here()` factory; parse `--here` flag in add/move commands |
|
||||||
|
| `StepManipulator.cs` | Handle `PositionType.Here` in `CalculateInsertIndex()` and `CalculateMoveTargetIndex()` |
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
**StepCommandParser.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Add to PositionType enum
|
||||||
|
public enum PositionType
|
||||||
|
{
|
||||||
|
At,
|
||||||
|
After,
|
||||||
|
Before,
|
||||||
|
First,
|
||||||
|
Last,
|
||||||
|
Here // NEW: Insert before current step (requires paused state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add factory method to StepPosition
|
||||||
|
public static StepPosition Here() => new StepPosition { Type = PositionType.Here };
|
||||||
|
|
||||||
|
// Update ToString()
|
||||||
|
PositionType.Here => "here",
|
||||||
|
|
||||||
|
// In ParseReplAddRunCommand and ParseReplAddUsesCommand, add case:
|
||||||
|
case "--here":
|
||||||
|
cmd.Position = StepPosition.Here();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Same for ParseReplMoveCommand
|
||||||
|
```
|
||||||
|
|
||||||
|
**StepManipulator.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// In CalculateInsertIndex():
|
||||||
|
case PositionType.Here:
|
||||||
|
{
|
||||||
|
// "Here" means before the current step
|
||||||
|
// Since current step is held separately (not in JobSteps queue),
|
||||||
|
// we need to:
|
||||||
|
// 1. Verify we're paused (have a current step)
|
||||||
|
// 2. Insert at position 0 of pending AND move current step after it
|
||||||
|
|
||||||
|
if (_currentStep == null)
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.InvalidPosition,
|
||||||
|
"Can only use --here when paused at a breakpoint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The new step goes at index 0, and we need to re-queue the current step
|
||||||
|
// Actually, we need a different approach - see "Special handling" below
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Special handling for `--here`:**
|
||||||
|
|
||||||
|
The current architecture has `_currentStep` held separately from `JobSteps`. To insert "before" the current step, we need to:
|
||||||
|
|
||||||
|
1. Insert the new step at position 0 of `JobSteps`
|
||||||
|
2. Move `_currentStep` back into `JobSteps` at position 1
|
||||||
|
3. Set the new step as `_currentStep`
|
||||||
|
|
||||||
|
Alternative (simpler): Modify `InsertStep` to handle `Here` specially:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public int InsertStep(IStep step, StepPosition position)
|
||||||
|
{
|
||||||
|
// Special case: --here inserts before current step
|
||||||
|
if (position.Type == PositionType.Here)
|
||||||
|
{
|
||||||
|
if (_currentStep == null)
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.InvalidPosition,
|
||||||
|
"Can only use --here when paused at a breakpoint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-queue current step at the front
|
||||||
|
var pending = _jobContext.JobSteps.ToList();
|
||||||
|
pending.Insert(0, _currentStep);
|
||||||
|
|
||||||
|
// Insert new step before it (at position 0)
|
||||||
|
pending.Insert(0, step);
|
||||||
|
|
||||||
|
// Clear and re-queue
|
||||||
|
_jobContext.JobSteps.Clear();
|
||||||
|
foreach (var s in pending)
|
||||||
|
_jobContext.JobSteps.Enqueue(s);
|
||||||
|
|
||||||
|
// New step becomes current
|
||||||
|
_currentStep = step;
|
||||||
|
|
||||||
|
// Track change and return index
|
||||||
|
var newIndex = _completedSteps.Count + 1;
|
||||||
|
// ... track change ...
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing logic for other position types ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] `steps add run "echo test" --here` when paused at step 1 inserts at position 1
|
||||||
|
- [ ] New step becomes the current step (shows as `▶` in list)
|
||||||
|
- [ ] Original current step moves to position 2
|
||||||
|
- [ ] Stepping forward runs the new step first
|
||||||
|
- [ ] `--here` when not paused returns appropriate error
|
||||||
|
- [ ] `steps move 3 --here` moves step 3 to before current step
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 2: `--id` Option for Step Identification
|
||||||
|
|
||||||
|
**Goal:** Allow users to specify a custom ID for dynamically added steps.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
| Flag | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `--id <identifier>` | Set the step's ID (used in `steps.<id>.outputs`, etc.) |
|
||||||
|
|
||||||
|
**Validation:**
|
||||||
|
- ID must be a non-empty string
|
||||||
|
- No format restrictions (matches YAML behavior - users can use any string)
|
||||||
|
|
||||||
|
**Duplicate handling:**
|
||||||
|
- If a step with the same ID already exists, return error: "Step with ID '<id>' already exists"
|
||||||
|
|
||||||
|
**Default behavior (unchanged):**
|
||||||
|
- If `--id` is not provided, auto-generate `_dynamic_<guid>` as before
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `StepCommandParser.cs` | Add `Id` property to `AddRunCommand` and `AddUsesCommand`; parse `--id` flag |
|
||||||
|
| `StepFactory.cs` | Add `id` parameter to `CreateRunStep()` and `CreateUsesStep()`; use provided ID or generate one |
|
||||||
|
| `StepCommandHandler.cs` | Pass `Id` from command to factory; validate uniqueness |
|
||||||
|
| `StepManipulator.cs` | Add `HasStepWithId(string id)` method for uniqueness check |
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
**StepCommandParser.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Add to AddRunCommand and AddUsesCommand classes:
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
// In ParseReplAddRunCommand and ParseReplAddUsesCommand:
|
||||||
|
case "--id":
|
||||||
|
cmd.Id = GetNextArg(tokens, ref i, "--id");
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
**StepFactory.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Update method signatures:
|
||||||
|
ActionStep CreateRunStep(
|
||||||
|
string script,
|
||||||
|
string id = null, // NEW
|
||||||
|
string name = null,
|
||||||
|
// ... rest unchanged
|
||||||
|
);
|
||||||
|
|
||||||
|
ActionStep CreateUsesStep(
|
||||||
|
string actionReference,
|
||||||
|
string id = null, // NEW
|
||||||
|
string name = null,
|
||||||
|
// ... rest unchanged
|
||||||
|
);
|
||||||
|
|
||||||
|
// In implementation:
|
||||||
|
public ActionStep CreateRunStep(string script, string id = null, ...)
|
||||||
|
{
|
||||||
|
var stepId = Guid.NewGuid();
|
||||||
|
var step = new ActionStep
|
||||||
|
{
|
||||||
|
Id = stepId,
|
||||||
|
Name = id ?? $"_dynamic_{stepId:N}", // Use provided ID or generate
|
||||||
|
DisplayName = name ?? "Run script",
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**StepManipulator.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Add method to check for duplicate IDs:
|
||||||
|
public bool HasStepWithId(string id)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check completed steps
|
||||||
|
foreach (var step in _completedSteps)
|
||||||
|
{
|
||||||
|
if (step is IActionRunner runner && runner.Action?.Name == id)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current step
|
||||||
|
if (_currentStep is IActionRunner currentRunner && currentRunner.Action?.Name == id)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Check pending steps
|
||||||
|
foreach (var step in _jobContext.JobSteps)
|
||||||
|
{
|
||||||
|
if (step is IActionRunner pendingRunner && pendingRunner.Action?.Name == id)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**StepCommandHandler.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// In HandleAddRunCommand and HandleAddUsesCommand:
|
||||||
|
if (!string.IsNullOrEmpty(cmd.Id) && _manipulator.HasStepWithId(cmd.Id))
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.DuplicateId,
|
||||||
|
$"Step with ID '{cmd.Id}' already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionStep = _factory.CreateRunStep(
|
||||||
|
cmd.Script,
|
||||||
|
cmd.Id, // NEW
|
||||||
|
cmd.Name,
|
||||||
|
// ...
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add step with custom ID
|
||||||
|
steps add run "echo hello" --id greet --name "Greeting"
|
||||||
|
|
||||||
|
# Reference in later step
|
||||||
|
steps add run "echo ${{ steps.greet.outputs.result }}"
|
||||||
|
|
||||||
|
# Duplicate ID returns error
|
||||||
|
steps add run "echo bye" --id greet
|
||||||
|
# Error: Step with ID 'greet' already exists
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] `steps add run "echo test" --id my_step` creates step with ID `my_step`
|
||||||
|
- [ ] Step ID appears correctly in `steps list` output
|
||||||
|
- [ ] Attempting duplicate ID returns clear error
|
||||||
|
- [ ] Omitting `--id` still generates `_dynamic_<guid>` IDs
|
||||||
|
- [ ] ID is correctly set on the underlying `ActionStep.Name` property
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 3: Help Commands (`--help`)
|
||||||
|
|
||||||
|
**Goal:** Add `--help` flag support to provide usage information for all step commands.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
| Command | Output |
|
||||||
|
|---------|--------|
|
||||||
|
| `steps` | List of available subcommands |
|
||||||
|
| `steps --help` | Same as above |
|
||||||
|
| `steps add --help` | Help for `add` command (shows `run` and `uses` subcommands) |
|
||||||
|
| `steps add run --help` | Help for `add run` with all options |
|
||||||
|
| `steps add uses --help` | Help for `add uses` with all options |
|
||||||
|
| `steps edit --help` | Help for `edit` command |
|
||||||
|
| `steps remove --help` | Help for `remove` command |
|
||||||
|
| `steps move --help` | Help for `move` command |
|
||||||
|
| `steps list --help` | Help for `list` command |
|
||||||
|
| `steps export --help` | Help for `export` command |
|
||||||
|
|
||||||
|
**Output format:** Text only (no JSON support needed)
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `StepCommandParser.cs` | Add `HelpCommand` class; detect `--help` flag and return appropriate help command |
|
||||||
|
| `StepCommandHandler.cs` | Add `HandleHelpCommand()` with help text for each command |
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
**StepCommandParser.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Add new command class:
|
||||||
|
public class HelpCommand : StepCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The command to show help for (null = top-level help)
|
||||||
|
/// </summary>
|
||||||
|
public string Command { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sub-command if applicable (e.g., "run" for "steps add run --help")
|
||||||
|
/// </summary>
|
||||||
|
public string SubCommand { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify ParseReplCommand to detect --help:
|
||||||
|
private StepCommand ParseReplCommand(string input)
|
||||||
|
{
|
||||||
|
var tokens = Tokenize(input);
|
||||||
|
|
||||||
|
// Handle bare "steps" command
|
||||||
|
if (tokens.Count == 1 && tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new HelpCommand { Command = null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
|
"Invalid command format. Expected: steps <command> [args...]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for --help anywhere in tokens
|
||||||
|
if (tokens.Contains("--help") || tokens.Contains("-h"))
|
||||||
|
{
|
||||||
|
return ParseHelpCommand(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCommand = tokens[1].ToLower();
|
||||||
|
// ... existing switch ...
|
||||||
|
}
|
||||||
|
|
||||||
|
private HelpCommand ParseHelpCommand(List<string> tokens)
|
||||||
|
{
|
||||||
|
// Remove --help/-h from tokens
|
||||||
|
tokens.RemoveAll(t => t == "--help" || t == "-h");
|
||||||
|
|
||||||
|
// "steps --help" or "steps"
|
||||||
|
if (tokens.Count == 1)
|
||||||
|
{
|
||||||
|
return new HelpCommand { Command = null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// "steps add --help"
|
||||||
|
var cmd = tokens[1].ToLower();
|
||||||
|
|
||||||
|
// "steps add run --help"
|
||||||
|
string subCmd = null;
|
||||||
|
if (tokens.Count >= 3 && (cmd == "add"))
|
||||||
|
{
|
||||||
|
subCmd = tokens[2].ToLower();
|
||||||
|
if (subCmd != "run" && subCmd != "uses")
|
||||||
|
subCmd = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HelpCommand { Command = cmd, SubCommand = subCmd };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**StepCommandHandler.cs:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private StepCommandResult HandleHelpCommand(HelpCommand cmd)
|
||||||
|
{
|
||||||
|
string helpText = (cmd.Command, cmd.SubCommand) switch
|
||||||
|
{
|
||||||
|
(null, _) => GetTopLevelHelp(),
|
||||||
|
("add", null) => GetAddHelp(),
|
||||||
|
("add", "run") => GetAddRunHelp(),
|
||||||
|
("add", "uses") => GetAddUsesHelp(),
|
||||||
|
("edit", _) => GetEditHelp(),
|
||||||
|
("remove", _) => GetRemoveHelp(),
|
||||||
|
("move", _) => GetMoveHelp(),
|
||||||
|
("list", _) => GetListHelp(),
|
||||||
|
("export", _) => GetExportHelp(),
|
||||||
|
_ => $"Unknown command: {cmd.Command}"
|
||||||
|
};
|
||||||
|
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = helpText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetTopLevelHelp() => @"
|
||||||
|
steps - Manipulate job steps during debug session
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
list Show all steps with status
|
||||||
|
add Add a new step (run or uses)
|
||||||
|
edit Modify a pending step
|
||||||
|
remove Delete a pending step
|
||||||
|
move Reorder a pending step
|
||||||
|
export Generate YAML for modified steps
|
||||||
|
|
||||||
|
Use 'steps <command> --help' for more information about a command.
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetAddHelp() => @"
|
||||||
|
steps add - Add a new step to the job
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps add run <script> [options] Add a shell command step
|
||||||
|
steps add uses <action> [options] Add an action step
|
||||||
|
|
||||||
|
Use 'steps add run --help' or 'steps add uses --help' for detailed options.
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetAddRunHelp() => @"
|
||||||
|
steps add run - Add a shell command step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps add run ""<script>"" [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--id <id> Step ID for referencing in expressions
|
||||||
|
--name ""<name>"" Display name for the step
|
||||||
|
--shell <shell> Shell to use (bash, sh, pwsh, python, cmd)
|
||||||
|
--working-directory <dir> Working directory for the script
|
||||||
|
--if ""<condition>"" Condition expression (default: success())
|
||||||
|
--env KEY=value Environment variable (can repeat)
|
||||||
|
--continue-on-error Don't fail job if step fails
|
||||||
|
--timeout <minutes> Step timeout in minutes
|
||||||
|
|
||||||
|
POSITION OPTIONS:
|
||||||
|
--here Insert before current step (default)
|
||||||
|
--after <index> Insert after step at index
|
||||||
|
--before <index> Insert before step at index
|
||||||
|
--at <index> Insert at specific index
|
||||||
|
--first Insert at first pending position
|
||||||
|
--last Insert at end of job
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps add run ""npm test""
|
||||||
|
steps add run ""echo hello"" --name ""Greeting"" --id greet
|
||||||
|
steps add run ""./build.sh"" --shell bash --after 3
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetAddUsesHelp() => @"
|
||||||
|
steps add uses - Add an action step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps add uses <action@ref> [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--id <id> Step ID for referencing in expressions
|
||||||
|
--name ""<name>"" Display name for the step
|
||||||
|
--with key=value Action input (can repeat)
|
||||||
|
--env KEY=value Environment variable (can repeat)
|
||||||
|
--if ""<condition>"" Condition expression (default: success())
|
||||||
|
--continue-on-error Don't fail job if step fails
|
||||||
|
--timeout <minutes> Step timeout in minutes
|
||||||
|
|
||||||
|
POSITION OPTIONS:
|
||||||
|
--here Insert before current step (default)
|
||||||
|
--after <index> Insert after step at index
|
||||||
|
--before <index> Insert before step at index
|
||||||
|
--at <index> Insert at specific index
|
||||||
|
--first Insert at first pending position
|
||||||
|
--last Insert at end of job
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps add uses actions/checkout@v4
|
||||||
|
steps add uses actions/setup-node@v4 --with node-version=20
|
||||||
|
steps add uses ./my-action --name ""Local Action"" --after 2
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetEditHelp() => @"
|
||||||
|
steps edit - Modify a pending step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps edit <index> [modifications]
|
||||||
|
|
||||||
|
MODIFICATIONS:
|
||||||
|
--name ""<name>"" Change display name
|
||||||
|
--script ""<script>"" Change script (run steps only)
|
||||||
|
--shell <shell> Change shell (run steps only)
|
||||||
|
--working-directory <dir> Change working directory
|
||||||
|
--if ""<condition>"" Change condition expression
|
||||||
|
--with key=value Set/update action input (uses steps only)
|
||||||
|
--env KEY=value Set/update environment variable
|
||||||
|
--remove-with <key> Remove action input
|
||||||
|
--remove-env <key> Remove environment variable
|
||||||
|
--continue-on-error Enable continue-on-error
|
||||||
|
--no-continue-on-error Disable continue-on-error
|
||||||
|
--timeout <minutes> Change timeout
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps edit 3 --name ""Updated Name""
|
||||||
|
steps edit 4 --script ""npm run test:ci""
|
||||||
|
steps edit 2 --env DEBUG=true --timeout 30
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetRemoveHelp() => @"
|
||||||
|
steps remove - Delete a pending step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps remove <index>
|
||||||
|
|
||||||
|
ARGUMENTS:
|
||||||
|
<index> 1-based index of the step to remove (must be pending)
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps remove 5
|
||||||
|
steps remove 3
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetMoveHelp() => @"
|
||||||
|
steps move - Reorder a pending step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps move <from> <position>
|
||||||
|
|
||||||
|
ARGUMENTS:
|
||||||
|
<from> 1-based index of the step to move (must be pending)
|
||||||
|
|
||||||
|
POSITION OPTIONS:
|
||||||
|
--here Move before current step
|
||||||
|
--after <index> Move after step at index
|
||||||
|
--before <index> Move before step at index
|
||||||
|
--to <index> Move to specific index
|
||||||
|
--first Move to first pending position
|
||||||
|
--last Move to end of job
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps move 5 --after 2
|
||||||
|
steps move 4 --first
|
||||||
|
steps move 3 --here
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetListHelp() => @"
|
||||||
|
steps list - Show all steps with status
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps list [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--verbose Show additional step details
|
||||||
|
--output json|text Output format (default: text)
|
||||||
|
|
||||||
|
OUTPUT:
|
||||||
|
Shows all steps with:
|
||||||
|
- Index number
|
||||||
|
- Status indicator (completed, current, pending)
|
||||||
|
- Step name
|
||||||
|
- Step type (run/uses) and details
|
||||||
|
- Change indicator ([ADDED], [MODIFIED], [MOVED])
|
||||||
|
".Trim();
|
||||||
|
|
||||||
|
private string GetExportHelp() => @"
|
||||||
|
steps export - Generate YAML for modified steps
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps export [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--changes-only Only export added/modified steps
|
||||||
|
--with-comments Include change markers as YAML comments
|
||||||
|
--output json|text Output format (default: text)
|
||||||
|
|
||||||
|
OUTPUT:
|
||||||
|
Generates valid YAML that can be pasted into a workflow file.
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps export
|
||||||
|
steps export --changes-only --with-comments
|
||||||
|
".Trim();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] `steps` shows top-level help
|
||||||
|
- [ ] `steps --help` shows top-level help
|
||||||
|
- [ ] `steps -h` shows top-level help
|
||||||
|
- [ ] `steps add --help` shows add command help
|
||||||
|
- [ ] `steps add run --help` shows add run help with all options
|
||||||
|
- [ ] `steps add uses --help` shows add uses help with all options
|
||||||
|
- [ ] `steps edit --help` shows edit help
|
||||||
|
- [ ] `steps remove --help` shows remove help
|
||||||
|
- [ ] `steps move --help` shows move help
|
||||||
|
- [ ] `steps list --help` shows list help
|
||||||
|
- [ ] `steps export --help` shows export help
|
||||||
|
- [ ] `--help` can appear anywhere in command (e.g., `steps add --help run`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 4: Browser Extension UI Updates
|
||||||
|
|
||||||
|
**Goal:** Update the Add Step form to use `--here` as default and add the ID field.
|
||||||
|
|
||||||
|
### Changes to `browser-ext/content/content.js`
|
||||||
|
|
||||||
|
#### 1. Update Position Dropdown
|
||||||
|
|
||||||
|
**Current options:**
|
||||||
|
- "At end (default)"
|
||||||
|
- "At first pending position"
|
||||||
|
- "After current step"
|
||||||
|
|
||||||
|
**New options:**
|
||||||
|
- "Before next step" (default) - uses `--here`
|
||||||
|
- "At end"
|
||||||
|
- "After current step"
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In showAddStepDialog():
|
||||||
|
<div class="dap-form-group">
|
||||||
|
<label class="dap-label">Position</label>
|
||||||
|
<select class="form-control dap-position-select">
|
||||||
|
<option value="here" selected>Before next step</option>
|
||||||
|
<option value="last">At end</option>
|
||||||
|
<option value="after">After current step</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Add ID Field
|
||||||
|
|
||||||
|
Add after the Name field:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
<div class="dap-form-group">
|
||||||
|
<label class="dap-label">ID (optional)</label>
|
||||||
|
<input type="text" class="form-control dap-id-input"
|
||||||
|
placeholder="my_step_id">
|
||||||
|
<span class="dap-help-text">Used to reference step outputs: steps.<id>.outputs</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Update `handleAddStep()`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function handleAddStep(modal) {
|
||||||
|
const type = modal.querySelector('.dap-step-type-select').value;
|
||||||
|
const name = modal.querySelector('.dap-name-input').value.trim() || undefined;
|
||||||
|
const id = modal.querySelector('.dap-id-input').value.trim() || undefined; // NEW
|
||||||
|
const positionSelect = modal.querySelector('.dap-position-select').value;
|
||||||
|
|
||||||
|
let position = {};
|
||||||
|
if (positionSelect === 'here') {
|
||||||
|
position.here = true; // NEW
|
||||||
|
} else if (positionSelect === 'after') {
|
||||||
|
const currentStep = stepsList.find((s) => s.status === 'current');
|
||||||
|
if (currentStep) {
|
||||||
|
position.after = currentStep.index;
|
||||||
|
} else {
|
||||||
|
position.here = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
position.last = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass id to sendStepCommand
|
||||||
|
result = await sendStepCommand('step.add', {
|
||||||
|
type: 'run',
|
||||||
|
script,
|
||||||
|
id, // NEW
|
||||||
|
name,
|
||||||
|
shell,
|
||||||
|
position,
|
||||||
|
});
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Update `buildAddStepCommand()`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function buildAddStepCommand(options) {
|
||||||
|
let cmd = 'steps add';
|
||||||
|
|
||||||
|
if (options.type === 'run') {
|
||||||
|
cmd += ` run ${quoteString(options.script)}`;
|
||||||
|
if (options.shell) cmd += ` --shell ${options.shell}`;
|
||||||
|
if (options.workingDirectory) cmd += ` --working-directory ${quoteString(options.workingDirectory)}`;
|
||||||
|
} else if (options.type === 'uses') {
|
||||||
|
cmd += ` uses ${options.action}`;
|
||||||
|
if (options.with) {
|
||||||
|
for (const [key, value] of Object.entries(options.with)) {
|
||||||
|
cmd += ` --with ${key}=${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.id) cmd += ` --id ${quoteString(options.id)}`; // NEW
|
||||||
|
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
|
||||||
|
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
|
||||||
|
// ... rest of options ...
|
||||||
|
|
||||||
|
// Position
|
||||||
|
if (options.position) {
|
||||||
|
if (options.position.here) cmd += ' --here'; // NEW
|
||||||
|
else if (options.position.after !== undefined) cmd += ` --after ${options.position.after}`;
|
||||||
|
else if (options.position.before !== undefined) cmd += ` --before ${options.position.before}`;
|
||||||
|
else if (options.position.at !== undefined) cmd += ` --at ${options.position.at}`;
|
||||||
|
else if (options.position.first) cmd += ' --first';
|
||||||
|
// --last is default, no need to specify
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Updates (`browser-ext/content/content.css`)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.dap-help-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--fgColor-muted, #8b949e);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [ ] Position dropdown defaults to "Before next step"
|
||||||
|
- [ ] ID field is visible and optional
|
||||||
|
- [ ] ID placeholder text is helpful
|
||||||
|
- [ ] Help text explains the purpose of ID
|
||||||
|
- [ ] Adding step with ID works correctly
|
||||||
|
- [ ] Adding step with "Before next step" uses `--here` flag
|
||||||
|
- [ ] Form validation doesn't require ID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Summary
|
||||||
|
|
||||||
|
### Files to Create
|
||||||
|
|
||||||
|
None - all changes are modifications to existing files.
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Chunks | Changes |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs` | 1, 2, 3 | Add `Here` position type, `Id` property, `HelpCommand` class |
|
||||||
|
| `src/Runner.Worker/Dap/StepCommands/StepManipulator.cs` | 1, 2 | Handle `Here` position, add `HasStepWithId()` method |
|
||||||
|
| `src/Runner.Worker/Dap/StepCommands/StepFactory.cs` | 2 | Add `id` parameter to create methods |
|
||||||
|
| `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs` | 2, 3 | Pass ID to factory, add help text handlers |
|
||||||
|
| `browser-ext/content/content.js` | 4 | Update form with ID field and position options |
|
||||||
|
| `browser-ext/content/content.css` | 4 | Add help text styling |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Messages
|
||||||
|
|
||||||
|
| Code | Message |
|
||||||
|
|------|---------|
|
||||||
|
| `INVALID_POSITION` | Can only use --here when paused at a breakpoint |
|
||||||
|
| `DUPLICATE_ID` | Step with ID '<id>' already exists |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estimated Effort
|
||||||
|
|
||||||
|
| Chunk | Effort |
|
||||||
|
|-------|--------|
|
||||||
|
| Chunk 1: `--here` position | ~1-2 hours |
|
||||||
|
| Chunk 2: `--id` option | ~1 hour |
|
||||||
|
| Chunk 3: Help commands | ~1-2 hours |
|
||||||
|
| Chunk 4: Browser extension UI | ~30 min |
|
||||||
|
| **Total** | **~4-5 hours** |
|
||||||
@@ -932,6 +932,13 @@ html[data-color-mode="light"] .dap-debug-btn.selected {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dap-help-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--fgColor-muted, #8b949e);
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.dap-modal .form-control {
|
.dap-modal .form-control {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--bgColor-inset, #010409) !important;
|
background-color: var(--bgColor-inset, #010409) !important;
|
||||||
|
|||||||
@@ -610,6 +610,7 @@ function buildAddStepCommand(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.id) cmd += ` --id ${quoteString(options.id)}`;
|
||||||
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
|
if (options.name) cmd += ` --name ${quoteString(options.name)}`;
|
||||||
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
|
if (options.if) cmd += ` --if ${quoteString(options.if)}`;
|
||||||
if (options.env) {
|
if (options.env) {
|
||||||
@@ -622,7 +623,8 @@ function buildAddStepCommand(options) {
|
|||||||
|
|
||||||
// Position
|
// Position
|
||||||
if (options.position) {
|
if (options.position) {
|
||||||
if (options.position.after !== undefined) cmd += ` --after ${options.position.after}`;
|
if (options.position.here) cmd += ' --here';
|
||||||
|
else if (options.position.after !== undefined) cmd += ` --after ${options.position.after}`;
|
||||||
else if (options.position.before !== undefined) cmd += ` --before ${options.position.before}`;
|
else if (options.position.before !== undefined) cmd += ` --before ${options.position.before}`;
|
||||||
else if (options.position.at !== undefined) cmd += ` --at ${options.position.at}`;
|
else if (options.position.at !== undefined) cmd += ` --at ${options.position.at}`;
|
||||||
else if (options.position.first) cmd += ' --first';
|
else if (options.position.first) cmd += ' --first';
|
||||||
@@ -917,11 +919,17 @@ function showAddStepDialog() {
|
|||||||
<input type="text" class="form-control dap-name-input" placeholder="Step name">
|
<input type="text" class="form-control dap-name-input" placeholder="Step name">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dap-form-group">
|
||||||
|
<label class="dap-label">ID (optional)</label>
|
||||||
|
<input type="text" class="form-control dap-id-input" placeholder="my_step_id">
|
||||||
|
<span class="dap-help-text">Used to reference step outputs: steps.<id>.outputs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="dap-form-group">
|
<div class="dap-form-group">
|
||||||
<label class="dap-label">Position</label>
|
<label class="dap-label">Position</label>
|
||||||
<select class="form-control dap-position-select">
|
<select class="form-control dap-position-select">
|
||||||
<option value="last">At end (default)</option>
|
<option value="here" selected>Before next step</option>
|
||||||
<option value="first">At first pending position</option>
|
<option value="last">At end</option>
|
||||||
<option value="after">After current step</option>
|
<option value="after">After current step</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -965,18 +973,19 @@ function showAddStepDialog() {
|
|||||||
async function handleAddStep(modal) {
|
async function handleAddStep(modal) {
|
||||||
const type = modal.querySelector('.dap-step-type-select').value;
|
const type = modal.querySelector('.dap-step-type-select').value;
|
||||||
const name = modal.querySelector('.dap-name-input').value.trim() || undefined;
|
const name = modal.querySelector('.dap-name-input').value.trim() || undefined;
|
||||||
|
const id = modal.querySelector('.dap-id-input').value.trim() || undefined;
|
||||||
const positionSelect = modal.querySelector('.dap-position-select').value;
|
const positionSelect = modal.querySelector('.dap-position-select').value;
|
||||||
|
|
||||||
let position = {};
|
let position = {};
|
||||||
if (positionSelect === 'first') {
|
if (positionSelect === 'here') {
|
||||||
position.first = true;
|
position.here = true;
|
||||||
} else if (positionSelect === 'after') {
|
} else if (positionSelect === 'after') {
|
||||||
// After current step - find current step index
|
// After current step - find current step index
|
||||||
const currentStep = stepsList.find((s) => s.status === 'current');
|
const currentStep = stepsList.find((s) => s.status === 'current');
|
||||||
if (currentStep) {
|
if (currentStep) {
|
||||||
position.after = currentStep.index;
|
position.after = currentStep.index;
|
||||||
} else {
|
} else {
|
||||||
position.last = true;
|
position.here = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
position.last = true;
|
position.last = true;
|
||||||
@@ -995,6 +1004,7 @@ async function handleAddStep(modal) {
|
|||||||
result = await sendStepCommand('step.add', {
|
result = await sendStepCommand('step.add', {
|
||||||
type: 'run',
|
type: 'run',
|
||||||
script,
|
script,
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
shell,
|
shell,
|
||||||
position,
|
position,
|
||||||
@@ -1022,6 +1032,7 @@ async function handleAddStep(modal) {
|
|||||||
result = await sendStepCommand('step.add', {
|
result = await sendStepCommand('step.add', {
|
||||||
type: 'uses',
|
type: 'uses',
|
||||||
action,
|
action,
|
||||||
|
id,
|
||||||
name,
|
name,
|
||||||
with: Object.keys(withInputs).length > 0 ? withInputs : undefined,
|
with: Object.keys(withInputs).length > 0 ? withInputs : undefined,
|
||||||
position,
|
position,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
var result = command switch
|
var result = command switch
|
||||||
{
|
{
|
||||||
|
HelpCommand help => HandleHelp(help),
|
||||||
ListCommand list => HandleList(list),
|
ListCommand list => HandleList(list),
|
||||||
AddRunCommand addRun => HandleAddRun(addRun, jobContext),
|
AddRunCommand addRun => HandleAddRun(addRun, jobContext),
|
||||||
AddUsesCommand addUses => await HandleAddUsesAsync(addUses, jobContext),
|
AddUsesCommand addUses => await HandleAddUsesAsync(addUses, jobContext),
|
||||||
@@ -111,6 +112,211 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
#region Command Handlers
|
#region Command Handlers
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the steps help command.
|
||||||
|
/// </summary>
|
||||||
|
private StepCommandResult HandleHelp(HelpCommand command)
|
||||||
|
{
|
||||||
|
string helpText = (command.Command, command.SubCommand) switch
|
||||||
|
{
|
||||||
|
(null, _) => GetTopLevelHelp(),
|
||||||
|
("add", null) => GetAddHelp(),
|
||||||
|
("add", "run") => GetAddRunHelp(),
|
||||||
|
("add", "uses") => GetAddUsesHelp(),
|
||||||
|
("edit", _) => GetEditHelp(),
|
||||||
|
("remove", _) => GetRemoveHelp(),
|
||||||
|
("move", _) => GetMoveHelp(),
|
||||||
|
("list", _) => GetListHelp(),
|
||||||
|
("export", _) => GetExportHelp(),
|
||||||
|
_ => $"Unknown command: {command.Command}. Use 'steps --help' for available commands."
|
||||||
|
};
|
||||||
|
|
||||||
|
return new StepCommandResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = helpText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Help Text Methods
|
||||||
|
|
||||||
|
private static string GetTopLevelHelp() =>
|
||||||
|
@"steps - Manipulate job steps during debug session
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
list Show all steps with status
|
||||||
|
add Add a new step (run or uses)
|
||||||
|
edit Modify a pending step
|
||||||
|
remove Delete a pending step
|
||||||
|
move Reorder a pending step
|
||||||
|
export Generate YAML for modified steps
|
||||||
|
|
||||||
|
Use 'steps <command> --help' for more information about a command.";
|
||||||
|
|
||||||
|
private static string GetAddHelp() =>
|
||||||
|
@"steps add - Add a new step to the job
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps add run <script> [options] Add a shell command step
|
||||||
|
steps add uses <action> [options] Add an action step
|
||||||
|
|
||||||
|
Use 'steps add run --help' or 'steps add uses --help' for detailed options.";
|
||||||
|
|
||||||
|
private static string GetAddRunHelp() =>
|
||||||
|
@"steps add run - Add a shell command step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps add run ""<script>"" [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--id <id> Step ID for referencing in expressions
|
||||||
|
--name ""<name>"" Display name for the step
|
||||||
|
--shell <shell> Shell to use (bash, sh, pwsh, python, cmd)
|
||||||
|
--working-directory <dir> Working directory for the script
|
||||||
|
--if ""<condition>"" Condition expression (default: success())
|
||||||
|
--env KEY=value Environment variable (can repeat)
|
||||||
|
--continue-on-error Don't fail job if step fails
|
||||||
|
--timeout <minutes> Step timeout in minutes
|
||||||
|
|
||||||
|
POSITION OPTIONS:
|
||||||
|
--here Insert before current step (default)
|
||||||
|
--after <index> Insert after step at index
|
||||||
|
--before <index> Insert before step at index
|
||||||
|
--at <index> Insert at specific index
|
||||||
|
--first Insert at first pending position
|
||||||
|
--last Insert at end of job
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps add run ""npm test""
|
||||||
|
steps add run ""echo hello"" --name ""Greeting"" --id greet
|
||||||
|
steps add run ""./build.sh"" --shell bash --after 3";
|
||||||
|
|
||||||
|
private static string GetAddUsesHelp() =>
|
||||||
|
@"steps add uses - Add an action step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps add uses <action@ref> [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--id <id> Step ID for referencing in expressions
|
||||||
|
--name ""<name>"" Display name for the step
|
||||||
|
--with key=value Action input (can repeat)
|
||||||
|
--env KEY=value Environment variable (can repeat)
|
||||||
|
--if ""<condition>"" Condition expression (default: success())
|
||||||
|
--continue-on-error Don't fail job if step fails
|
||||||
|
--timeout <minutes> Step timeout in minutes
|
||||||
|
|
||||||
|
POSITION OPTIONS:
|
||||||
|
--here Insert before current step (default)
|
||||||
|
--after <index> Insert after step at index
|
||||||
|
--before <index> Insert before step at index
|
||||||
|
--at <index> Insert at specific index
|
||||||
|
--first Insert at first pending position
|
||||||
|
--last Insert at end of job
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps add uses actions/checkout@v4
|
||||||
|
steps add uses actions/setup-node@v4 --with node-version=20
|
||||||
|
steps add uses ./my-action --name ""Local Action"" --after 2";
|
||||||
|
|
||||||
|
private static string GetEditHelp() =>
|
||||||
|
@"steps edit - Modify a pending step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps edit <index> [modifications]
|
||||||
|
|
||||||
|
MODIFICATIONS:
|
||||||
|
--name ""<name>"" Change display name
|
||||||
|
--script ""<script>"" Change script (run steps only)
|
||||||
|
--shell <shell> Change shell (run steps only)
|
||||||
|
--working-directory <dir> Change working directory
|
||||||
|
--if ""<condition>"" Change condition expression
|
||||||
|
--with key=value Set/update action input (uses steps only)
|
||||||
|
--env KEY=value Set/update environment variable
|
||||||
|
--remove-with <key> Remove action input
|
||||||
|
--remove-env <key> Remove environment variable
|
||||||
|
--continue-on-error Enable continue-on-error
|
||||||
|
--no-continue-on-error Disable continue-on-error
|
||||||
|
--timeout <minutes> Change timeout
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps edit 3 --name ""Updated Name""
|
||||||
|
steps edit 4 --script ""npm run test:ci""
|
||||||
|
steps edit 2 --env DEBUG=true --timeout 30";
|
||||||
|
|
||||||
|
private static string GetRemoveHelp() =>
|
||||||
|
@"steps remove - Delete a pending step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps remove <index>
|
||||||
|
|
||||||
|
ARGUMENTS:
|
||||||
|
<index> 1-based index of the step to remove (must be pending)
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps remove 5
|
||||||
|
steps remove 3";
|
||||||
|
|
||||||
|
private static string GetMoveHelp() =>
|
||||||
|
@"steps move - Reorder a pending step
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps move <from> <position>
|
||||||
|
|
||||||
|
ARGUMENTS:
|
||||||
|
<from> 1-based index of the step to move (must be pending)
|
||||||
|
|
||||||
|
POSITION OPTIONS:
|
||||||
|
--here Move before current step
|
||||||
|
--after <index> Move after step at index
|
||||||
|
--before <index> Move before step at index
|
||||||
|
--to <index> Move to specific index
|
||||||
|
--first Move to first pending position
|
||||||
|
--last Move to end of job
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps move 5 --after 2
|
||||||
|
steps move 4 --first
|
||||||
|
steps move 3 --here";
|
||||||
|
|
||||||
|
private static string GetListHelp() =>
|
||||||
|
@"steps list - Show all steps with status
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps list [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--verbose Show additional step details
|
||||||
|
--output json|text Output format (default: text)
|
||||||
|
|
||||||
|
OUTPUT:
|
||||||
|
Shows all steps with:
|
||||||
|
- Index number
|
||||||
|
- Status indicator (completed, current, pending)
|
||||||
|
- Step name
|
||||||
|
- Step type (run/uses) and details
|
||||||
|
- Change indicator ([ADDED], [MODIFIED], [MOVED])";
|
||||||
|
|
||||||
|
private static string GetExportHelp() =>
|
||||||
|
@"steps export - Generate YAML for modified steps
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
steps export [options]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--changes-only Only export added/modified steps
|
||||||
|
--with-comments Include change markers as YAML comments
|
||||||
|
--output json|text Output format (default: text)
|
||||||
|
|
||||||
|
OUTPUT:
|
||||||
|
Generates valid YAML that can be pasted into a workflow file.
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
steps export
|
||||||
|
steps export --changes-only --with-comments";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the steps list command.
|
/// Handles the steps list command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -227,9 +433,17 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private StepCommandResult HandleAddRun(AddRunCommand command, IExecutionContext jobContext)
|
private StepCommandResult HandleAddRun(AddRunCommand command, IExecutionContext jobContext)
|
||||||
{
|
{
|
||||||
|
// Validate step ID uniqueness
|
||||||
|
if (!string.IsNullOrEmpty(command.Id) && _manipulator.HasStepWithId(command.Id))
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.DuplicateId,
|
||||||
|
$"Step with ID '{command.Id}' already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
// Create the ActionStep
|
// Create the ActionStep
|
||||||
var actionStep = _factory.CreateRunStep(
|
var actionStep = _factory.CreateRunStep(
|
||||||
script: command.Script,
|
script: command.Script,
|
||||||
|
id: command.Id,
|
||||||
name: command.Name,
|
name: command.Name,
|
||||||
shell: command.Shell,
|
shell: command.Shell,
|
||||||
workingDirectory: command.WorkingDirectory,
|
workingDirectory: command.WorkingDirectory,
|
||||||
@@ -290,9 +504,17 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<StepCommandResult> HandleAddUsesAsync(AddUsesCommand command, IExecutionContext jobContext)
|
private async Task<StepCommandResult> HandleAddUsesAsync(AddUsesCommand command, IExecutionContext jobContext)
|
||||||
{
|
{
|
||||||
|
// Validate step ID uniqueness
|
||||||
|
if (!string.IsNullOrEmpty(command.Id) && _manipulator.HasStepWithId(command.Id))
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.DuplicateId,
|
||||||
|
$"Step with ID '{command.Id}' already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
// Create the ActionStep
|
// Create the ActionStep
|
||||||
var actionStep = _factory.CreateUsesStep(
|
var actionStep = _factory.CreateUsesStep(
|
||||||
actionReference: command.Action,
|
actionReference: command.Action,
|
||||||
|
id: command.Id,
|
||||||
name: command.Name,
|
name: command.Name,
|
||||||
with: command.With,
|
with: command.With,
|
||||||
env: command.Env,
|
env: command.Env,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using GitHub.Runner.Common;
|
using GitHub.Runner.Common;
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
public class AddRunCommand : StepCommand
|
public class AddRunCommand : StepCommand
|
||||||
{
|
{
|
||||||
public string Script { get; set; }
|
public string Script { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Shell { get; set; }
|
public string Shell { get; set; }
|
||||||
public string WorkingDirectory { get; set; }
|
public string WorkingDirectory { get; set; }
|
||||||
@@ -81,6 +83,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
public class AddUsesCommand : StepCommand
|
public class AddUsesCommand : StepCommand
|
||||||
{
|
{
|
||||||
public string Action { get; set; }
|
public string Action { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public Dictionary<string, string> With { get; set; }
|
public Dictionary<string, string> With { get; set; }
|
||||||
public Dictionary<string, string> Env { get; set; }
|
public Dictionary<string, string> Env { get; set; }
|
||||||
@@ -136,6 +139,23 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
public bool WithComments { get; set; }
|
public bool WithComments { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// steps [command] --help
|
||||||
|
/// Shows help information for step commands.
|
||||||
|
/// </summary>
|
||||||
|
public class HelpCommand : StepCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The command to show help for (null = top-level help)
|
||||||
|
/// </summary>
|
||||||
|
public string Command { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sub-command if applicable (e.g., "run" for "steps add run --help")
|
||||||
|
/// </summary>
|
||||||
|
public string SubCommand { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Position Types
|
#region Position Types
|
||||||
@@ -154,7 +174,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// <summary>Insert at first pending position</summary>
|
/// <summary>Insert at first pending position</summary>
|
||||||
First,
|
First,
|
||||||
/// <summary>Insert at end (default)</summary>
|
/// <summary>Insert at end (default)</summary>
|
||||||
Last
|
Last,
|
||||||
|
/// <summary>Insert before current step (requires paused state)</summary>
|
||||||
|
Here
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -170,6 +192,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
public static StepPosition Before(int index) => new StepPosition { Type = PositionType.Before, Index = index };
|
public static StepPosition Before(int index) => new StepPosition { Type = PositionType.Before, Index = index };
|
||||||
public static StepPosition First() => new StepPosition { Type = PositionType.First };
|
public static StepPosition First() => new StepPosition { Type = PositionType.First };
|
||||||
public static StepPosition Last() => new StepPosition { Type = PositionType.Last };
|
public static StepPosition Last() => new StepPosition { Type = PositionType.Last };
|
||||||
|
public static StepPosition Here() => new StepPosition { Type = PositionType.Here };
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
@@ -180,6 +203,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
PositionType.Before => $"before {Index}",
|
PositionType.Before => $"before {Index}",
|
||||||
PositionType.First => "first",
|
PositionType.First => "first",
|
||||||
PositionType.Last => "last",
|
PositionType.Last => "last",
|
||||||
|
PositionType.Here => "here",
|
||||||
_ => "unknown"
|
_ => "unknown"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -225,12 +249,25 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
// Tokenize the input, respecting quoted strings
|
// Tokenize the input, respecting quoted strings
|
||||||
var tokens = Tokenize(input);
|
var tokens = Tokenize(input);
|
||||||
|
|
||||||
|
// Handle bare "steps" command - show top-level help
|
||||||
|
if (tokens.Count == 1 && tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new HelpCommand { Command = null };
|
||||||
|
}
|
||||||
|
|
||||||
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
if (tokens.Count < 2 || !tokens[0].Equals("steps", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Invalid command format. Expected: steps <command> [args...]");
|
"Invalid command format. Expected: steps <command> [args...]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for --help or -h anywhere in tokens
|
||||||
|
if (tokens.Any(t => t.Equals("--help", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
t.Equals("-h", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return ParseHelpCommand(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
var subCommand = tokens[1].ToLower();
|
var subCommand = tokens[1].ToLower();
|
||||||
|
|
||||||
return subCommand switch
|
return subCommand switch
|
||||||
@@ -363,6 +400,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
switch (opt)
|
switch (opt)
|
||||||
{
|
{
|
||||||
|
case "--id":
|
||||||
|
cmd.Id = GetNextArg(tokens, ref i, "--id");
|
||||||
|
break;
|
||||||
case "--name":
|
case "--name":
|
||||||
cmd.Name = GetNextArg(tokens, ref i, "--name");
|
cmd.Name = GetNextArg(tokens, ref i, "--name");
|
||||||
break;
|
break;
|
||||||
@@ -399,6 +439,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
case "--last":
|
case "--last":
|
||||||
cmd.Position = StepPosition.Last();
|
cmd.Position = StepPosition.Last();
|
||||||
break;
|
break;
|
||||||
|
case "--here":
|
||||||
|
cmd.Position = StepPosition.Here();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
||||||
$"Unknown option: {tokens[i]}");
|
$"Unknown option: {tokens[i]}");
|
||||||
@@ -434,6 +477,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
|
|
||||||
switch (opt)
|
switch (opt)
|
||||||
{
|
{
|
||||||
|
case "--id":
|
||||||
|
cmd.Id = GetNextArg(tokens, ref i, "--id");
|
||||||
|
break;
|
||||||
case "--name":
|
case "--name":
|
||||||
cmd.Name = GetNextArg(tokens, ref i, "--name");
|
cmd.Name = GetNextArg(tokens, ref i, "--name");
|
||||||
break;
|
break;
|
||||||
@@ -467,6 +513,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
case "--last":
|
case "--last":
|
||||||
cmd.Position = StepPosition.Last();
|
cmd.Position = StepPosition.Last();
|
||||||
break;
|
break;
|
||||||
|
case "--here":
|
||||||
|
cmd.Position = StepPosition.Here();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
||||||
$"Unknown option: {tokens[i]}");
|
$"Unknown option: {tokens[i]}");
|
||||||
@@ -638,6 +687,9 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
case "--last":
|
case "--last":
|
||||||
cmd.Position = StepPosition.Last();
|
cmd.Position = StepPosition.Last();
|
||||||
break;
|
break;
|
||||||
|
case "--here":
|
||||||
|
cmd.Position = StepPosition.Here();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
throw new StepCommandException(StepCommandErrors.InvalidOption,
|
||||||
$"Unknown option: {tokens[i]}");
|
$"Unknown option: {tokens[i]}");
|
||||||
@@ -647,7 +699,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
if (cmd.Position == null)
|
if (cmd.Position == null)
|
||||||
{
|
{
|
||||||
throw new StepCommandException(StepCommandErrors.ParseError,
|
throw new StepCommandException(StepCommandErrors.ParseError,
|
||||||
"Move command requires a position (--to, --after, --before, --first, or --last)");
|
"Move command requires a position (--to, --after, --before, --first, --last, or --here)");
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd;
|
return cmd;
|
||||||
@@ -681,6 +733,41 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a help command from tokens containing --help or -h.
|
||||||
|
/// </summary>
|
||||||
|
private HelpCommand ParseHelpCommand(List<string> tokens)
|
||||||
|
{
|
||||||
|
// Create a copy to avoid modifying the original
|
||||||
|
var workingTokens = new List<string>(tokens);
|
||||||
|
|
||||||
|
// Remove --help/-h from tokens
|
||||||
|
workingTokens.RemoveAll(t => t.Equals("--help", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
t.Equals("-h", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
// "steps --help" or just "steps" (after removing --help)
|
||||||
|
if (workingTokens.Count <= 1)
|
||||||
|
{
|
||||||
|
return new HelpCommand { Command = null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// "steps <command> --help"
|
||||||
|
var cmd = workingTokens[1].ToLower();
|
||||||
|
|
||||||
|
// Check for "steps add run --help" or "steps add uses --help"
|
||||||
|
string subCmd = null;
|
||||||
|
if (workingTokens.Count >= 3 && cmd == "add")
|
||||||
|
{
|
||||||
|
var possibleSubCmd = workingTokens[2].ToLower();
|
||||||
|
if (possibleSubCmd == "run" || possibleSubCmd == "uses")
|
||||||
|
{
|
||||||
|
subCmd = possibleSubCmd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HelpCommand { Command = cmd, SubCommand = subCmd };
|
||||||
|
}
|
||||||
|
|
||||||
#region Argument Helpers
|
#region Argument Helpers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -66,10 +66,12 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
public const string InvalidCommand = "INVALID_COMMAND";
|
public const string InvalidCommand = "INVALID_COMMAND";
|
||||||
public const string InvalidOption = "INVALID_OPTION";
|
public const string InvalidOption = "INVALID_OPTION";
|
||||||
public const string InvalidType = "INVALID_TYPE";
|
public const string InvalidType = "INVALID_TYPE";
|
||||||
|
public const string InvalidPosition = "INVALID_POSITION";
|
||||||
public const string ActionDownloadFailed = "ACTION_DOWNLOAD_FAILED";
|
public const string ActionDownloadFailed = "ACTION_DOWNLOAD_FAILED";
|
||||||
public const string ParseError = "PARSE_ERROR";
|
public const string ParseError = "PARSE_ERROR";
|
||||||
public const string NotPaused = "NOT_PAUSED";
|
public const string NotPaused = "NOT_PAUSED";
|
||||||
public const string NoContext = "NO_CONTEXT";
|
public const string NoContext = "NO_CONTEXT";
|
||||||
|
public const string DuplicateId = "DUPLICATE_ID";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// Creates a new run step (script step).
|
/// Creates a new run step (script step).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="script">The script to execute</param>
|
/// <param name="script">The script to execute</param>
|
||||||
|
/// <param name="id">Optional step ID for referencing in expressions (e.g., steps.<id>.outputs)</param>
|
||||||
/// <param name="name">Optional display name for the step</param>
|
/// <param name="name">Optional display name for the step</param>
|
||||||
/// <param name="shell">Optional shell (bash, sh, pwsh, python, etc.)</param>
|
/// <param name="shell">Optional shell (bash, sh, pwsh, python, etc.)</param>
|
||||||
/// <param name="workingDirectory">Optional working directory</param>
|
/// <param name="workingDirectory">Optional working directory</param>
|
||||||
@@ -28,6 +29,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// <returns>A configured ActionStep with ScriptReference</returns>
|
/// <returns>A configured ActionStep with ScriptReference</returns>
|
||||||
ActionStep CreateRunStep(
|
ActionStep CreateRunStep(
|
||||||
string script,
|
string script,
|
||||||
|
string id = null,
|
||||||
string name = null,
|
string name = null,
|
||||||
string shell = null,
|
string shell = null,
|
||||||
string workingDirectory = null,
|
string workingDirectory = null,
|
||||||
@@ -40,6 +42,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// Creates a new uses step (action step).
|
/// Creates a new uses step (action step).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="actionReference">The action reference (e.g., "actions/checkout@v4", "owner/repo@ref", "./local-action")</param>
|
/// <param name="actionReference">The action reference (e.g., "actions/checkout@v4", "owner/repo@ref", "./local-action")</param>
|
||||||
|
/// <param name="id">Optional step ID for referencing in expressions (e.g., steps.<id>.outputs)</param>
|
||||||
/// <param name="name">Optional display name for the step</param>
|
/// <param name="name">Optional display name for the step</param>
|
||||||
/// <param name="with">Optional input parameters for the action</param>
|
/// <param name="with">Optional input parameters for the action</param>
|
||||||
/// <param name="env">Optional environment variables</param>
|
/// <param name="env">Optional environment variables</param>
|
||||||
@@ -49,6 +52,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// <returns>A configured ActionStep with RepositoryPathReference or ContainerRegistryReference</returns>
|
/// <returns>A configured ActionStep with RepositoryPathReference or ContainerRegistryReference</returns>
|
||||||
ActionStep CreateUsesStep(
|
ActionStep CreateUsesStep(
|
||||||
string actionReference,
|
string actionReference,
|
||||||
|
string id = null,
|
||||||
string name = null,
|
string name = null,
|
||||||
Dictionary<string, string> with = null,
|
Dictionary<string, string> with = null,
|
||||||
Dictionary<string, string> env = null,
|
Dictionary<string, string> env = null,
|
||||||
@@ -132,6 +136,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ActionStep CreateRunStep(
|
public ActionStep CreateRunStep(
|
||||||
string script,
|
string script,
|
||||||
|
string id = null,
|
||||||
string name = null,
|
string name = null,
|
||||||
string shell = null,
|
string shell = null,
|
||||||
string workingDirectory = null,
|
string workingDirectory = null,
|
||||||
@@ -149,7 +154,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
var step = new ActionStep
|
var step = new ActionStep
|
||||||
{
|
{
|
||||||
Id = stepId,
|
Id = stepId,
|
||||||
Name = $"_dynamic_{stepId:N}",
|
Name = id ?? $"_dynamic_{stepId:N}",
|
||||||
DisplayName = name ?? "Run script",
|
DisplayName = name ?? "Run script",
|
||||||
Reference = new ScriptReference(),
|
Reference = new ScriptReference(),
|
||||||
Condition = condition ?? "success()",
|
Condition = condition ?? "success()",
|
||||||
@@ -183,6 +188,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ActionStep CreateUsesStep(
|
public ActionStep CreateUsesStep(
|
||||||
string actionReference,
|
string actionReference,
|
||||||
|
string id = null,
|
||||||
string name = null,
|
string name = null,
|
||||||
Dictionary<string, string> with = null,
|
Dictionary<string, string> with = null,
|
||||||
Dictionary<string, string> env = null,
|
Dictionary<string, string> env = null,
|
||||||
@@ -201,7 +207,7 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
var step = new ActionStep
|
var step = new ActionStep
|
||||||
{
|
{
|
||||||
Id = stepId,
|
Id = stepId,
|
||||||
Name = $"_dynamic_{stepId:N}",
|
Name = id ?? $"_dynamic_{stepId:N}",
|
||||||
DisplayName = name ?? actionReference,
|
DisplayName = name ?? actionReference,
|
||||||
Condition = condition ?? "success()",
|
Condition = condition ?? "success()",
|
||||||
Enabled = true
|
Enabled = true
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
/// Clears all recorded changes (for testing or reset).
|
/// Clears all recorded changes (for testing or reset).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void ClearChanges();
|
void ClearChanges();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a step with the given ID already exists.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The step ID to check for.</param>
|
||||||
|
/// <returns>True if a step with that ID exists, false otherwise.</returns>
|
||||||
|
bool HasStepWithId(string id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -250,6 +257,12 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
ArgUtil.NotNull(step, nameof(step));
|
ArgUtil.NotNull(step, nameof(step));
|
||||||
ValidateInitialized();
|
ValidateInitialized();
|
||||||
|
|
||||||
|
// Special case: --here inserts before current step
|
||||||
|
if (position.Type == PositionType.Here)
|
||||||
|
{
|
||||||
|
return InsertStepHere(step);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the insertion index within the pending queue (0-based)
|
// Calculate the insertion index within the pending queue (0-based)
|
||||||
int insertAt = CalculateInsertIndex(position);
|
int insertAt = CalculateInsertIndex(position);
|
||||||
|
|
||||||
@@ -285,6 +298,57 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
return newIndex;
|
return newIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inserts a step before the current step (the one paused at a breakpoint).
|
||||||
|
/// The new step becomes the next step to run when the user continues/steps forward.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="step">The step to insert.</param>
|
||||||
|
/// <returns>The 1-based index where the step was inserted.</returns>
|
||||||
|
private int InsertStepHere(IStep step)
|
||||||
|
{
|
||||||
|
if (_currentStep == null)
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.InvalidPosition,
|
||||||
|
"Can only use --here when paused at a breakpoint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert queue to list for manipulation
|
||||||
|
var pending = _jobContext.JobSteps.ToList();
|
||||||
|
_jobContext.JobSteps.Clear();
|
||||||
|
|
||||||
|
// Re-queue the current step at the front of pending
|
||||||
|
pending.Insert(0, _currentStep);
|
||||||
|
|
||||||
|
// Insert the new step before it (at position 0)
|
||||||
|
pending.Insert(0, step);
|
||||||
|
|
||||||
|
// Re-queue all steps
|
||||||
|
foreach (var s in pending)
|
||||||
|
{
|
||||||
|
_jobContext.JobSteps.Enqueue(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The new step becomes the current step
|
||||||
|
_currentStep = step;
|
||||||
|
|
||||||
|
// Calculate the 1-based index (new step takes current step's position)
|
||||||
|
var newIndex = _completedSteps.Count + 1;
|
||||||
|
|
||||||
|
// Track the change
|
||||||
|
var stepInfo = StepInfo.FromStep(step, newIndex, StepStatus.Current);
|
||||||
|
stepInfo.Change = ChangeType.Added;
|
||||||
|
|
||||||
|
if (step is IActionRunner runner && runner.Action != null)
|
||||||
|
{
|
||||||
|
_addedStepIds.Add(runner.Action.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_changes.Add(StepChange.Added(stepInfo, newIndex));
|
||||||
|
|
||||||
|
Trace.Info($"Inserted step '{step.DisplayName}' at position {newIndex} (--here, before current step)");
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void RemoveStep(int index)
|
public void RemoveStep(int index)
|
||||||
{
|
{
|
||||||
@@ -323,6 +387,12 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
ValidateInitialized();
|
ValidateInitialized();
|
||||||
var stepInfo = ValidatePendingIndex(fromIndex);
|
var stepInfo = ValidatePendingIndex(fromIndex);
|
||||||
|
|
||||||
|
// Special case: --here moves step to before current step
|
||||||
|
if (position.Type == PositionType.Here)
|
||||||
|
{
|
||||||
|
return MoveStepHere(fromIndex, stepInfo);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate queue indices - BEFORE modifying the queue
|
// Calculate queue indices - BEFORE modifying the queue
|
||||||
var firstPendingIndex = GetFirstPendingIndex();
|
var firstPendingIndex = GetFirstPendingIndex();
|
||||||
var fromQueueIndex = fromIndex - firstPendingIndex;
|
var fromQueueIndex = fromIndex - firstPendingIndex;
|
||||||
@@ -364,6 +434,64 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
return newIndex;
|
return newIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a step to before the current step (the one paused at a breakpoint).
|
||||||
|
/// The moved step becomes the next step to run when the user continues/steps forward.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fromIndex">The 1-based index of the step to move.</param>
|
||||||
|
/// <param name="stepInfo">The StepInfo for the step being moved.</param>
|
||||||
|
/// <returns>The new 1-based index of the step.</returns>
|
||||||
|
private int MoveStepHere(int fromIndex, StepInfo stepInfo)
|
||||||
|
{
|
||||||
|
if (_currentStep == null)
|
||||||
|
{
|
||||||
|
throw new StepCommandException(StepCommandErrors.InvalidPosition,
|
||||||
|
"Can only use --here when paused at a breakpoint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate queue indices - BEFORE modifying the queue
|
||||||
|
var firstPendingIndex = GetFirstPendingIndex();
|
||||||
|
var fromQueueIndex = fromIndex - firstPendingIndex;
|
||||||
|
|
||||||
|
// Convert queue to list
|
||||||
|
var pending = _jobContext.JobSteps.ToList();
|
||||||
|
_jobContext.JobSteps.Clear();
|
||||||
|
|
||||||
|
// Remove the step from its original position
|
||||||
|
var step = pending[fromQueueIndex];
|
||||||
|
pending.RemoveAt(fromQueueIndex);
|
||||||
|
|
||||||
|
// Re-queue the current step at the front of pending
|
||||||
|
pending.Insert(0, _currentStep);
|
||||||
|
|
||||||
|
// Insert the moved step before the current step (at position 0)
|
||||||
|
pending.Insert(0, step);
|
||||||
|
|
||||||
|
// Re-queue all steps
|
||||||
|
foreach (var s in pending)
|
||||||
|
{
|
||||||
|
_jobContext.JobSteps.Enqueue(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The moved step becomes the current step
|
||||||
|
_currentStep = step;
|
||||||
|
|
||||||
|
// Calculate the new 1-based index (step takes current step's position)
|
||||||
|
var newIndex = _completedSteps.Count + 1;
|
||||||
|
|
||||||
|
// Track the change
|
||||||
|
var originalInfo = StepInfo.FromStep(step, fromIndex, StepStatus.Pending);
|
||||||
|
_changes.Add(StepChange.Moved(originalInfo, newIndex));
|
||||||
|
|
||||||
|
if (step is IActionRunner runner && runner.Action != null)
|
||||||
|
{
|
||||||
|
_modifiedStepIds.Add(runner.Action.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Info($"Moved step '{step.DisplayName}' from position {fromIndex} to {newIndex} (--here, before current step)");
|
||||||
|
return newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void EditStep(int index, Action<ActionStep> edit)
|
public void EditStep(int index, Action<ActionStep> edit)
|
||||||
{
|
{
|
||||||
@@ -423,6 +551,36 @@ namespace GitHub.Runner.Worker.Dap.StepCommands
|
|||||||
_originalSteps = null;
|
_originalSteps = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool HasStepWithId(string id)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check completed steps
|
||||||
|
foreach (var step in _completedSteps)
|
||||||
|
{
|
||||||
|
if (step is IActionRunner runner && runner.Action?.Name == id)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check current step
|
||||||
|
if (_currentStep is IActionRunner currentRunner && currentRunner.Action?.Name == id)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Check pending steps
|
||||||
|
if (_jobContext?.JobSteps != null)
|
||||||
|
{
|
||||||
|
foreach (var step in _jobContext.JobSteps)
|
||||||
|
{
|
||||||
|
if (step is IActionRunner pendingRunner && pendingRunner.Action?.Name == id)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#region Helper Methods
|
#region Helper Methods
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -376,6 +376,19 @@ namespace GitHub.Runner.Common.Tests.Worker.Dap.StepCommands
|
|||||||
Assert.Equal(3, addCmd.Position.Index);
|
Assert.Equal(3, addCmd.Position.Index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void Parse_AddRunCommand_PositionHere()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var cmd = _parser.Parse("steps add run \"echo here\" --here");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var addCmd = Assert.IsType<AddRunCommand>(cmd);
|
||||||
|
Assert.Equal(PositionType.Here, addCmd.Position.Type);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
@@ -420,6 +433,20 @@ namespace GitHub.Runner.Common.Tests.Worker.Dap.StepCommands
|
|||||||
Assert.Equal("npm", addCmd.With["cache"]);
|
Assert.Equal("npm", addCmd.With["cache"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void Parse_AddUsesCommand_PositionHere()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var cmd = _parser.Parse("steps add uses actions/checkout@v4 --here");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var addCmd = Assert.IsType<AddUsesCommand>(cmd);
|
||||||
|
Assert.Equal("actions/checkout@v4", addCmd.Action);
|
||||||
|
Assert.Equal(PositionType.Here, addCmd.Position.Type);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
@@ -596,6 +623,19 @@ namespace GitHub.Runner.Common.Tests.Worker.Dap.StepCommands
|
|||||||
Assert.Equal(3, moveCmd.Position.Index);
|
Assert.Equal(3, moveCmd.Position.Index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Level", "L0")]
|
||||||
|
[Trait("Category", "Worker")]
|
||||||
|
public void Parse_MoveCommand_Here()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var cmd = _parser.Parse("steps move 5 --here");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var moveCmd = Assert.IsType<MoveCommand>(cmd);
|
||||||
|
Assert.Equal(PositionType.Here, moveCmd.Position.Type);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[Trait("Level", "L0")]
|
[Trait("Level", "L0")]
|
||||||
[Trait("Category", "Worker")]
|
[Trait("Category", "Worker")]
|
||||||
|
|||||||
Reference in New Issue
Block a user