mirror of
https://github.com/actions/runner.git
synced 2026-01-23 04:51:23 +08:00
1267 lines
37 KiB
Markdown
1267 lines
37 KiB
Markdown
# Dynamic Step Manipulation & Workflow Export
|
|
|
|
**Status:** Draft
|
|
**Author:** GitHub Actions Team
|
|
**Date:** January 2026
|
|
**Prerequisites:** dap-debugging.md, dap-step-backwards.md (completed)
|
|
|
|
## Progress Checklist
|
|
|
|
- [x] **Chunk 1:** Command Parser & Infrastructure
|
|
- [x] **Chunk 2:** Step Serializer (ActionStep → YAML)
|
|
- [x] **Chunk 3:** Step Factory (Create new steps)
|
|
- [x] **Chunk 4:** Step Manipulator (Queue operations)
|
|
- [x] **Chunk 5:** REPL Commands (steps list, steps add run, steps edit, steps remove, steps move)
|
|
- [x] **Chunk 6:** Action Download Integration (steps add uses)
|
|
- [x] **Chunk 7:** Export Command (steps export)
|
|
- [x] **Chunk 8:** Output Format Flag (--output text|json for programmatic use)
|
|
- [x] **Chunk 9:** Browser Extension UI
|
|
|
|
## Overview
|
|
|
|
This plan extends the DAP debugger with the ability to dynamically manipulate job steps during a debug session: add new steps, edit pending steps, remove steps, and reorder them. At the end of a session, users can export the modified steps as YAML to paste into their workflow file.
|
|
|
|
This transforms the debugger from a "read-only inspection tool" into an **interactive workflow editor** — the key differentiator of this prototype.
|
|
|
|
## Goals
|
|
|
|
- **Primary:** Enable add/edit/move/delete of job steps during debug session
|
|
- **Primary:** Support both `run` and `uses` step types
|
|
- **Primary:** Export modified steps as valid YAML
|
|
- **Secondary:** Provide `--output` flag for text/JSON response format
|
|
- **Non-goal:** Full workflow file reconstruction (steps section only)
|
|
- **Non-goal:** Production action restriction enforcement (noted for later)
|
|
|
|
## Command API Specification
|
|
|
|
### Grammar
|
|
|
|
```
|
|
steps <command> [target] [options] [--output text|json]
|
|
```
|
|
|
|
### Output Format
|
|
|
|
All commands support the `--output` flag to control response format:
|
|
- `--output text` (default) - Human-readable text output
|
|
- `--output json` - JSON output for programmatic use
|
|
- Short form: `-o json`, `-o text`
|
|
- Equals form: `--output=json`, `--output=text`
|
|
|
|
### Index Reference
|
|
|
|
- **1-based indexing** for user-friendliness
|
|
- Completed steps are shown but read-only
|
|
- Cannot modify currently executing step (except via step-back)
|
|
|
|
### Commands Summary
|
|
|
|
| Command | Purpose | Example |
|
|
|---------|---------|---------|
|
|
| `steps list` | Show all steps | `steps list --verbose` |
|
|
| `steps add` | Add new step | `steps add run "npm test" --after 3` |
|
|
| `steps edit` | Modify step | `steps edit 4 --script "npm run test:ci"` |
|
|
| `steps remove` | Delete step | `steps remove 5` |
|
|
| `steps move` | Reorder step | `steps move 5 --after 2` |
|
|
| `steps export` | Generate YAML | `steps export --with-comments` |
|
|
|
|
### Position Modifiers
|
|
|
|
For `steps add` and `steps move`:
|
|
- `--at <index>` — Insert at specific position
|
|
- `--after <index>` — Insert after step
|
|
- `--before <index>` — Insert before step
|
|
- `--first` — Insert at first pending position
|
|
- `--last` — Insert at end (default)
|
|
|
|
### Full Command Reference
|
|
|
|
See "Command API Full Reference" section at end of document.
|
|
|
|
---
|
|
|
|
## Implementation Chunks
|
|
|
|
### Chunk 1: Command Parser & Infrastructure
|
|
|
|
**Goal:** Create the foundation for parsing and dispatching step commands.
|
|
|
|
**Files to create:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs`
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandResult.cs`
|
|
|
|
**Files to modify:**
|
|
- `src/Runner.Worker/Dap/DapDebugSession.cs` — Add command dispatch in `HandleEvaluate()`
|
|
|
|
**Details:**
|
|
|
|
1. **StepCommandParser** — Parse REPL command strings into structured commands:
|
|
```csharp
|
|
public interface IStepCommandParser
|
|
{
|
|
StepCommand Parse(string input); // "steps add run \"echo hello\" --after 3"
|
|
bool IsStepCommand(string input); // Starts with "steps " or equals "steps"
|
|
}
|
|
|
|
public enum OutputFormat { Text, Json }
|
|
|
|
public abstract class StepCommand
|
|
{
|
|
public OutputFormat Output { get; set; } = OutputFormat.Text;
|
|
}
|
|
public class ListCommand : StepCommand { public bool Verbose; }
|
|
public class AddRunCommand : StepCommand {
|
|
public string Script;
|
|
public string Name;
|
|
public string Shell;
|
|
public StepPosition Position;
|
|
// ...
|
|
}
|
|
public class AddUsesCommand : StepCommand { /* ... */ }
|
|
public class EditCommand : StepCommand { /* ... */ }
|
|
public class RemoveCommand : StepCommand { public int Index; }
|
|
public class MoveCommand : StepCommand { public int FromIndex; public StepPosition Position; }
|
|
public class ExportCommand : StepCommand { public bool ChangesOnly; public bool WithComments; }
|
|
```
|
|
|
|
2. **StepPosition** — Represent insertion position:
|
|
```csharp
|
|
public class StepPosition
|
|
{
|
|
public PositionType Type { get; set; } // At, After, Before, First, Last
|
|
public int? Index { get; set; } // For At, After, Before
|
|
}
|
|
```
|
|
|
|
3. **StepCommandResult** — Standardized response:
|
|
```csharp
|
|
public class StepCommandResult
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Message { get; set; }
|
|
public string Error { get; set; } // Error code
|
|
public object Result { get; set; } // Command-specific data
|
|
}
|
|
```
|
|
|
|
4. **Integration in DapDebugSession.HandleEvaluate():**
|
|
```csharp
|
|
// After checking for !debug command
|
|
if (_stepCommandParser.IsStepCommand(expression))
|
|
{
|
|
return await HandleStepCommandAsync(expression, executionContext);
|
|
}
|
|
```
|
|
|
|
**Testing:**
|
|
- Unit tests for command parsing
|
|
- Test various edge cases (quoted strings, escapes, missing args)
|
|
|
|
**Estimated effort:** Small-medium
|
|
|
|
---
|
|
|
|
### Chunk 2: Step Serializer (ActionStep → YAML)
|
|
|
|
**Goal:** Convert `Pipelines.ActionStep` objects to YAML string representation.
|
|
|
|
**Files to create:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepSerializer.cs`
|
|
|
|
**Details:**
|
|
|
|
1. **IStepSerializer interface:**
|
|
```csharp
|
|
public interface IStepSerializer
|
|
{
|
|
string ToYaml(Pipelines.ActionStep step);
|
|
string ToYaml(IEnumerable<StepInfo> steps, bool withComments = false);
|
|
}
|
|
```
|
|
|
|
2. **Handle both step types:**
|
|
|
|
For `run` steps (ScriptReference):
|
|
```yaml
|
|
- name: Run Tests
|
|
run: |
|
|
npm ci
|
|
npm test
|
|
shell: bash
|
|
working-directory: src
|
|
env:
|
|
NODE_ENV: test
|
|
if: success()
|
|
continue-on-error: false
|
|
timeout-minutes: 10
|
|
```
|
|
|
|
For `uses` steps (RepositoryPathReference):
|
|
```yaml
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
cache: npm
|
|
env:
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
if: success()
|
|
```
|
|
|
|
3. **Extract data from ActionStep:**
|
|
- `Reference` type determines run vs uses
|
|
- `Inputs` TemplateToken contains script (for run) or with values (for uses)
|
|
- `Environment` TemplateToken contains env vars
|
|
- `Condition` string contains if expression
|
|
- `DisplayName` or `DisplayNameToken` for name
|
|
|
|
4. **Use existing YAML infrastructure:**
|
|
- Consider using `YamlObjectWriter` from WorkflowParser
|
|
- Or use a simple string builder for more control
|
|
|
|
**Testing:**
|
|
- Round-trip tests: create step → serialize → verify YAML
|
|
- Test all step properties
|
|
- Test multi-line scripts
|
|
|
|
**Estimated effort:** Medium
|
|
|
|
---
|
|
|
|
### Chunk 3: Step Factory (Create new steps)
|
|
|
|
**Goal:** Create `Pipelines.ActionStep` and `IActionRunner` objects at runtime.
|
|
|
|
**Files to create:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepFactory.cs`
|
|
|
|
**Details:**
|
|
|
|
1. **IStepFactory interface:**
|
|
```csharp
|
|
public interface IStepFactory : IRunnerService
|
|
{
|
|
Pipelines.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);
|
|
|
|
Pipelines.ActionStep CreateUsesStep(
|
|
string actionReference, // "owner/repo@ref"
|
|
string name = null,
|
|
Dictionary<string, string> with = null,
|
|
Dictionary<string, string> env = null,
|
|
string condition = null,
|
|
bool continueOnError = false,
|
|
int? timeoutMinutes = null);
|
|
|
|
IActionRunner WrapInRunner(
|
|
Pipelines.ActionStep step,
|
|
IExecutionContext jobContext,
|
|
ActionRunStage stage = ActionRunStage.Main);
|
|
}
|
|
```
|
|
|
|
2. **CreateRunStep implementation:**
|
|
```csharp
|
|
public Pipelines.ActionStep CreateRunStep(...)
|
|
{
|
|
var step = new Pipelines.ActionStep
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = $"_dynamic_{Guid.NewGuid():N}",
|
|
DisplayName = name ?? "Run script",
|
|
Reference = new ScriptReference(),
|
|
Condition = condition ?? "success()",
|
|
ContinueOnError = CreateBoolToken(continueOnError),
|
|
TimeoutInMinutes = CreateIntToken(timeoutMinutes)
|
|
};
|
|
|
|
// 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);
|
|
|
|
return step;
|
|
}
|
|
```
|
|
|
|
3. **CreateUsesStep implementation:**
|
|
```csharp
|
|
public Pipelines.ActionStep CreateUsesStep(string actionReference, ...)
|
|
{
|
|
var (name, ref_, path) = ParseActionReference(actionReference);
|
|
|
|
var step = new Pipelines.ActionStep
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = $"_dynamic_{Guid.NewGuid():N}",
|
|
DisplayName = displayName ?? actionReference,
|
|
Reference = new RepositoryPathReference
|
|
{
|
|
Name = name, // "actions/checkout"
|
|
Ref = ref_, // "v4"
|
|
Path = path, // null or "subdir"
|
|
RepositoryType = "GitHub"
|
|
},
|
|
Condition = condition ?? "success()"
|
|
};
|
|
|
|
// Build with inputs
|
|
if (with?.Count > 0)
|
|
step.Inputs = CreateWithInputs(with);
|
|
|
|
return step;
|
|
}
|
|
```
|
|
|
|
4. **ParseActionReference helper:**
|
|
- `actions/checkout@v4` → name=`actions/checkout`, ref=`v4`, path=null
|
|
- `actions/setup-node@v4` → name=`actions/setup-node`, ref=`v4`
|
|
- `owner/repo/subdir@ref` → name=`owner/repo`, ref=`ref`, path=`subdir`
|
|
- `docker://alpine:latest` → ContainerRegistryReference instead
|
|
|
|
5. **WrapInRunner:** Create IActionRunner from ActionStep (copy pattern from JobExtension.cs):
|
|
```csharp
|
|
public IActionRunner WrapInRunner(Pipelines.ActionStep step, IExecutionContext jobContext, ActionRunStage stage)
|
|
{
|
|
var runner = HostContext.CreateService<IActionRunner>();
|
|
runner.Action = step;
|
|
runner.Stage = stage;
|
|
runner.Condition = step.Condition;
|
|
runner.ExecutionContext = jobContext.CreateChild(
|
|
Guid.NewGuid(),
|
|
step.DisplayName,
|
|
step.Name
|
|
);
|
|
return runner;
|
|
}
|
|
```
|
|
|
|
**Testing:**
|
|
- Create run step → verify all properties
|
|
- Create uses step → verify reference parsing
|
|
- Test edge cases (missing shell, local actions, docker actions)
|
|
|
|
**Estimated effort:** Medium
|
|
|
|
---
|
|
|
|
### Chunk 4: Step Manipulator (Queue operations)
|
|
|
|
**Goal:** Manipulate the job step queue (add, remove, move, track changes).
|
|
|
|
**Files to create:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepManipulator.cs`
|
|
- `src/Runner.Worker/Dap/StepCommands/StepInfo.cs`
|
|
- `src/Runner.Worker/Dap/StepCommands/StepChange.cs`
|
|
|
|
**Details:**
|
|
|
|
1. **StepInfo** — Unified step representation:
|
|
```csharp
|
|
public class StepInfo
|
|
{
|
|
public int Index { get; set; }
|
|
public string Name { get; set; }
|
|
public string Type { get; set; } // "run" or "uses"
|
|
public string TypeDetail { get; set; } // action ref or script preview
|
|
public StepStatus Status { get; set; } // Completed, Current, Pending
|
|
public ChangeType? Change { get; set; } // Added, Modified, null
|
|
public Pipelines.ActionStep Action { get; set; }
|
|
public IStep Step { get; set; }
|
|
}
|
|
|
|
public enum StepStatus { Completed, Current, Pending }
|
|
public enum ChangeType { Added, Modified, Removed, Moved }
|
|
```
|
|
|
|
2. **StepChange** — Track modifications:
|
|
```csharp
|
|
public class StepChange
|
|
{
|
|
public ChangeType Type { get; set; }
|
|
public int OriginalIndex { get; set; }
|
|
public StepInfo OriginalStep { get; set; }
|
|
public StepInfo ModifiedStep { get; set; }
|
|
}
|
|
```
|
|
|
|
3. **IStepManipulator interface:**
|
|
```csharp
|
|
public interface IStepManipulator : IRunnerService
|
|
{
|
|
// Initialize with job context
|
|
void Initialize(IExecutionContext jobContext, int currentStepIndex);
|
|
void UpdateCurrentIndex(int index);
|
|
|
|
// Query
|
|
IReadOnlyList<StepInfo> GetAllSteps();
|
|
StepInfo GetStep(int index);
|
|
int GetPendingCount();
|
|
int GetFirstPendingIndex();
|
|
|
|
// Mutate
|
|
int InsertStep(IStep step, StepPosition position);
|
|
void RemoveStep(int index);
|
|
void MoveStep(int fromIndex, StepPosition position);
|
|
void EditStep(int index, Action<Pipelines.ActionStep> edit);
|
|
|
|
// Change tracking
|
|
IReadOnlyList<StepChange> GetChanges();
|
|
void RecordOriginalState(); // Call at session start
|
|
}
|
|
```
|
|
|
|
4. **Implementation details:**
|
|
- Maintain list of completed steps (from checkpoints/history)
|
|
- Access `jobContext.JobSteps` queue for pending steps
|
|
- Track all modifications for export diff
|
|
- Validate indices before operations
|
|
|
|
5. **Queue manipulation:**
|
|
```csharp
|
|
public int InsertStep(IStep step, StepPosition position)
|
|
{
|
|
// Convert queue to list
|
|
var pending = _jobContext.JobSteps.ToList();
|
|
_jobContext.JobSteps.Clear();
|
|
|
|
// Calculate insertion index
|
|
int insertAt = CalculateInsertIndex(position, pending.Count);
|
|
|
|
// Insert
|
|
pending.Insert(insertAt, step);
|
|
|
|
// Re-queue
|
|
foreach (var s in pending)
|
|
_jobContext.JobSteps.Enqueue(s);
|
|
|
|
// Track change
|
|
_changes.Add(new StepChange { Type = ChangeType.Added, ... });
|
|
|
|
return _currentIndex + insertAt + 1; // Return 1-based index
|
|
}
|
|
```
|
|
|
|
**Testing:**
|
|
- Insert at various positions
|
|
- Remove and verify queue state
|
|
- Move operations
|
|
- Change tracking accuracy
|
|
|
|
**Estimated effort:** Medium
|
|
|
|
---
|
|
|
|
### Chunk 5: REPL Commands (run steps)
|
|
|
|
**Goal:** Implement `steps list`, `steps add run`, `steps edit`, `steps remove`, `steps move`.
|
|
|
|
**Files to modify:**
|
|
- `src/Runner.Worker/Dap/DapDebugSession.cs` — Add `HandleStepCommandAsync()`
|
|
|
|
**Files to create:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
|
|
|
**Details:**
|
|
|
|
1. **IStepCommandHandler interface:**
|
|
```csharp
|
|
public interface IStepCommandHandler : IRunnerService
|
|
{
|
|
Task<StepCommandResult> HandleAsync(StepCommand command, IExecutionContext context);
|
|
}
|
|
```
|
|
|
|
2. **Command implementations:**
|
|
|
|
**List:**
|
|
```csharp
|
|
case ListCommand list:
|
|
var steps = _manipulator.GetAllSteps();
|
|
var output = FormatStepList(steps, list.Verbose);
|
|
return new StepCommandResult { Success = true, Result = steps, Message = output };
|
|
```
|
|
|
|
**Add run:**
|
|
```csharp
|
|
case AddRunCommand add:
|
|
var actionStep = _factory.CreateRunStep(
|
|
add.Script, add.Name, add.Shell, add.WorkingDirectory,
|
|
add.Env, add.Condition, add.ContinueOnError, add.Timeout);
|
|
var runner = _factory.WrapInRunner(actionStep, context);
|
|
var index = _manipulator.InsertStep(runner, add.Position);
|
|
return new StepCommandResult {
|
|
Success = true,
|
|
Message = $"Step added at position {index}",
|
|
Result = new { index, step = GetStepInfo(runner) }
|
|
};
|
|
```
|
|
|
|
**Edit:**
|
|
```csharp
|
|
case EditCommand edit:
|
|
ValidatePendingIndex(edit.Index);
|
|
_manipulator.EditStep(edit.Index, step => {
|
|
if (edit.Script != null) UpdateScript(step, edit.Script);
|
|
if (edit.Name != null) step.DisplayName = edit.Name;
|
|
if (edit.Condition != null) step.Condition = edit.Condition;
|
|
// ... other fields
|
|
});
|
|
return new StepCommandResult { Success = true, Message = $"Step {edit.Index} updated" };
|
|
```
|
|
|
|
**Remove:**
|
|
```csharp
|
|
case RemoveCommand remove:
|
|
ValidatePendingIndex(remove.Index);
|
|
_manipulator.RemoveStep(remove.Index);
|
|
return new StepCommandResult { Success = true, Message = $"Step {remove.Index} removed" };
|
|
```
|
|
|
|
**Move:**
|
|
```csharp
|
|
case MoveCommand move:
|
|
ValidatePendingIndex(move.FromIndex);
|
|
var newIndex = _manipulator.MoveStep(move.FromIndex, move.Position);
|
|
return new StepCommandResult { Success = true, Message = $"Step moved to position {newIndex}" };
|
|
```
|
|
|
|
3. **Integration in DapDebugSession:**
|
|
```csharp
|
|
private async Task<Response> HandleStepCommandAsync(string expression, IExecutionContext context)
|
|
{
|
|
try
|
|
{
|
|
var command = _commandParser.Parse(expression);
|
|
var result = await _commandHandler.HandleAsync(command, context);
|
|
|
|
if (result.Success)
|
|
return CreateSuccessResponse(new EvaluateResponseBody
|
|
{
|
|
Result = result.Message,
|
|
Type = "string"
|
|
});
|
|
else
|
|
return CreateErrorResponse($"{result.Error}: {result.Message}");
|
|
}
|
|
catch (StepCommandException ex)
|
|
{
|
|
return CreateErrorResponse(ex.Message);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Testing:**
|
|
- End-to-end: add step → list → verify
|
|
- Edit various properties
|
|
- Remove and verify indices shift
|
|
- Move operations
|
|
|
|
**Estimated effort:** Medium-large
|
|
|
|
---
|
|
|
|
### Chunk 6: Action Download Integration (steps add uses)
|
|
|
|
**Goal:** Support `steps add uses` with full action download.
|
|
|
|
**Files to modify:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
|
|
|
**Details:**
|
|
|
|
1. **Add uses command handling:**
|
|
```csharp
|
|
case AddUsesCommand add:
|
|
// Create the step
|
|
var actionStep = _factory.CreateUsesStep(
|
|
add.Action, add.Name, add.With, add.Env,
|
|
add.Condition, add.ContinueOnError, add.Timeout);
|
|
|
|
// Download the action (this is the key difference from run steps)
|
|
var actionManager = HostContext.GetService<IActionManager>();
|
|
var prepareResult = await actionManager.PrepareActionsAsync(
|
|
context,
|
|
new[] { actionStep }
|
|
);
|
|
|
|
// Check for pre-steps (some actions have setup steps)
|
|
if (prepareResult.PreStepTracker.TryGetValue(actionStep.Id, out var preStep))
|
|
{
|
|
// Insert pre-step before main step
|
|
var preRunner = _factory.WrapInRunner(preStep.Action, context, ActionRunStage.Pre);
|
|
_manipulator.InsertStep(preRunner, add.Position);
|
|
}
|
|
|
|
// Wrap and insert main step
|
|
var runner = _factory.WrapInRunner(actionStep, context);
|
|
var index = _manipulator.InsertStep(runner, CalculateMainPosition(add.Position, hasPreStep));
|
|
|
|
// Handle post-steps (cleanup)
|
|
// These go to PostJobSteps stack, handled by existing infrastructure
|
|
|
|
return new StepCommandResult {
|
|
Success = true,
|
|
Message = $"Action '{add.Action}' added at position {index}",
|
|
Result = new { index, step = GetStepInfo(runner) }
|
|
};
|
|
```
|
|
|
|
2. **Error handling:**
|
|
- Action not found → clear error message with action name
|
|
- Network failure → suggest retry
|
|
- Invalid action reference format → parse error
|
|
|
|
3. **Production note (for future):**
|
|
```csharp
|
|
// TODO: Before production release, add action restriction checks:
|
|
// - Verify action is in organization's allowed list
|
|
// - Check verified creator requirements
|
|
// - Enforce enterprise policies
|
|
// For now, allow all actions in prototype
|
|
```
|
|
|
|
**Testing:**
|
|
- Add common actions (checkout, setup-node)
|
|
- Test actions with pre/post steps
|
|
- Test local actions (./.github/actions/...)
|
|
- Test docker actions (docker://...)
|
|
- Error cases: invalid action, network failure
|
|
|
|
**Estimated effort:** Medium
|
|
|
|
---
|
|
|
|
### Chunk 7: Export Command (steps export)
|
|
|
|
**Goal:** Generate YAML output for modified steps.
|
|
|
|
**Files to modify:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
|
|
|
**Details:**
|
|
|
|
1. **Export command handling:**
|
|
```csharp
|
|
case ExportCommand export:
|
|
var steps = _manipulator.GetAllSteps();
|
|
var changes = _manipulator.GetChanges();
|
|
|
|
IEnumerable<StepInfo> toExport;
|
|
if (export.ChangesOnly)
|
|
{
|
|
toExport = steps.Where(s => s.Change != null);
|
|
}
|
|
else
|
|
{
|
|
toExport = steps;
|
|
}
|
|
|
|
var yaml = _serializer.ToYaml(toExport, export.WithComments);
|
|
|
|
return new StepCommandResult
|
|
{
|
|
Success = true,
|
|
Message = yaml,
|
|
Result = new {
|
|
yaml,
|
|
totalSteps = steps.Count,
|
|
addedCount = changes.Count(c => c.Type == ChangeType.Added),
|
|
modifiedCount = changes.Count(c => c.Type == ChangeType.Modified)
|
|
}
|
|
};
|
|
```
|
|
|
|
2. **YAML output format:**
|
|
```yaml
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node # ADDED
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
|
|
- name: Run Tests # MODIFIED
|
|
run: |
|
|
npm ci
|
|
npm test
|
|
shell: bash
|
|
```
|
|
|
|
3. **Change comments (when --with-comments):**
|
|
- `# ADDED` for new steps
|
|
- `# MODIFIED` for edited steps
|
|
- Removed steps listed at bottom as comments (optional)
|
|
|
|
**Testing:**
|
|
- Export with no changes → valid YAML
|
|
- Export with additions → ADDED comments
|
|
- Export with modifications → MODIFIED comments
|
|
- Verify YAML is valid and can be pasted into workflow
|
|
|
|
**Estimated effort:** Small
|
|
|
|
---
|
|
|
|
### Chunk 8: Output Format Flag for Browser Extension
|
|
|
|
**Goal:** Add `--output` flag support for programmatic access (replaces separate JSON API).
|
|
|
|
**Files to modify:**
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs`
|
|
- `src/Runner.Worker/Dap/StepCommands/StepCommandHandler.cs`
|
|
|
|
**Details:**
|
|
|
|
1. **Add OutputFormat enum and property to base StepCommand:**
|
|
```csharp
|
|
public enum OutputFormat { Text, Json }
|
|
|
|
public abstract class StepCommand
|
|
{
|
|
public OutputFormat Output { get; set; } = OutputFormat.Text;
|
|
}
|
|
```
|
|
|
|
2. **Parse `--output` flag in command parser:**
|
|
```csharp
|
|
// Recognize --output json, --output text, -o json, -o text, --output=json
|
|
private OutputFormat ParseOutputFlag(List<string> tokens)
|
|
{
|
|
for (int i = 0; i < tokens.Count; i++)
|
|
{
|
|
var token = tokens[i].ToLower();
|
|
if (token == "--output" || token == "-o")
|
|
{
|
|
if (i + 1 < tokens.Count)
|
|
{
|
|
var format = tokens[i + 1].ToLower();
|
|
tokens.RemoveAt(i); tokens.RemoveAt(i);
|
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
|
}
|
|
}
|
|
else if (token.StartsWith("--output="))
|
|
{
|
|
var format = token.Substring("--output=".Length);
|
|
tokens.RemoveAt(i);
|
|
return format == "json" ? OutputFormat.Json : OutputFormat.Text;
|
|
}
|
|
}
|
|
return OutputFormat.Text;
|
|
}
|
|
```
|
|
|
|
3. **Format responses based on Output property in handler:**
|
|
```csharp
|
|
if (command.Output == OutputFormat.Json)
|
|
{
|
|
return new StepCommandResult
|
|
{
|
|
Success = true,
|
|
Message = JsonConvert.SerializeObject(new { Success = true, Result = data })
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new StepCommandResult
|
|
{
|
|
Success = true,
|
|
Message = FormatAsText(data)
|
|
};
|
|
}
|
|
```
|
|
|
|
4. **Browser extension sends REPL commands with `--output json`:**
|
|
```javascript
|
|
// Browser extension builds command strings like:
|
|
// "steps list --output json"
|
|
// "steps add run \"echo test\" --after 3 --output json"
|
|
```
|
|
|
|
**Benefits over separate JSON API:**
|
|
- Single code path for parsing all commands
|
|
- Easier debugging (UI sends same commands humans would type)
|
|
- Commands can be copy-pasted from UI to console for testing
|
|
- Less code to maintain
|
|
|
|
**Testing:**
|
|
- All commands with `--output json` return valid JSON
|
|
- All commands with `--output text` (or default) return human-readable text
|
|
- Short form `-o json` works correctly
|
|
|
|
**Estimated effort:** Small
|
|
|
|
---
|
|
|
|
### Chunk 9: Browser Extension UI
|
|
|
|
**Goal:** Add step manipulation UI to the browser extension.
|
|
|
|
**Files to modify:**
|
|
- `browser-ext/content/content.js`
|
|
- `browser-ext/content/content.css`
|
|
- `browser-ext/background/background.js`
|
|
|
|
**Details:**
|
|
|
|
1. **Steps Panel in Debugger Pane:**
|
|
```html
|
|
<div class="dap-steps-panel">
|
|
<div class="dap-steps-header">
|
|
<span>Steps</span>
|
|
<button class="dap-add-step-btn">+ Add</button>
|
|
</div>
|
|
<div class="dap-steps-list">
|
|
<!-- Steps rendered here -->
|
|
</div>
|
|
<div class="dap-steps-footer">
|
|
<button class="dap-export-btn">Export Changes</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
2. **Step List Rendering:**
|
|
```javascript
|
|
function renderSteps(steps) {
|
|
return steps.map(step => `
|
|
<div class="dap-step ${step.status}" data-index="${step.index}">
|
|
<span class="dap-step-status">${getStatusIcon(step.status)}</span>
|
|
<span class="dap-step-index">${step.index}.</span>
|
|
<span class="dap-step-name">${step.name}</span>
|
|
${step.change ? `<span class="dap-step-badge">[${step.change}]</span>` : ''}
|
|
<span class="dap-step-type">${step.type}</span>
|
|
${step.status === 'pending' ? renderStepActions(step) : ''}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
```
|
|
|
|
3. **Add Step Dialog:**
|
|
```javascript
|
|
function showAddStepDialog() {
|
|
// Modal with:
|
|
// - Type selector: run / uses
|
|
// - For run: script textarea, shell dropdown
|
|
// - For uses: action input with autocomplete
|
|
// - Common: name, if condition, env vars
|
|
// - Position: dropdown (after current, at end, at position)
|
|
}
|
|
```
|
|
|
|
4. **Step Context Menu:**
|
|
```javascript
|
|
function showStepContextMenu(stepIndex, event) {
|
|
// Edit, Move Up, Move Down, Delete
|
|
}
|
|
```
|
|
|
|
5. **Export Modal:**
|
|
```javascript
|
|
function showExportModal(yaml) {
|
|
// Code view with syntax highlighting
|
|
// Copy to clipboard button
|
|
}
|
|
```
|
|
|
|
6. **Send commands via REPL format with `--output json`:**
|
|
```javascript
|
|
async function addStep(type, options) {
|
|
const cmd = buildStepCommand('step.add', { type, ...options });
|
|
const response = await sendEvaluate(cmd); // e.g., "steps add run \"echo test\" --output json"
|
|
refreshStepList();
|
|
}
|
|
|
|
async function loadSteps() {
|
|
const response = await sendEvaluate('steps list --output json');
|
|
// Parse JSON response
|
|
}
|
|
```
|
|
|
|
**Testing:**
|
|
- Manual testing in Chrome
|
|
- All UI operations work correctly
|
|
- Responsive layout
|
|
|
|
**Estimated effort:** Medium-large
|
|
|
|
---
|
|
|
|
## File Summary
|
|
|
|
### New Files
|
|
|
|
| File | Chunk | Purpose |
|
|
|------|-------|---------|
|
|
| `StepCommands/StepCommandParser.cs` | 1 | Parse REPL and JSON commands |
|
|
| `StepCommands/StepCommandResult.cs` | 1 | Standardized command responses |
|
|
| `StepCommands/StepSerializer.cs` | 2 | ActionStep → YAML conversion |
|
|
| `StepCommands/StepFactory.cs` | 3 | Create new ActionStep objects |
|
|
| `StepCommands/StepManipulator.cs` | 4 | Queue operations & change tracking |
|
|
| `StepCommands/StepInfo.cs` | 4 | Step info data structure |
|
|
| `StepCommands/StepChange.cs` | 4 | Change tracking data structure |
|
|
| `StepCommands/StepCommandHandler.cs` | 5 | Command execution logic |
|
|
|
|
### Modified Files
|
|
|
|
| File | Chunk | Changes |
|
|
|------|-------|---------|
|
|
| `DapDebugSession.cs` | 1, 5 | Add command dispatch, wire up services |
|
|
| `StepCommandParser.cs` | 8 | Add `--output` flag parsing |
|
|
| `StepCommandHandler.cs` | 8 | Format responses based on OutputFormat |
|
|
| `content.js` | 9 | Steps panel, dialogs, export modal, build REPL commands |
|
|
| `content.css` | 9 | Styling for new UI elements |
|
|
| `background.js` | 9 | Helper functions if needed |
|
|
|
|
---
|
|
|
|
## Command API Full Reference
|
|
|
|
### Output Format Flag
|
|
|
|
All commands support the `--output` flag:
|
|
|
|
```
|
|
steps <command> ... --output text # Human-readable output (default)
|
|
steps <command> ... --output json # JSON output for programmatic use
|
|
steps <command> ... -o json # Short form
|
|
steps <command> ... --output=json # Equals form
|
|
```
|
|
|
|
The browser extension uses `--output json` for all commands to receive structured responses.
|
|
|
|
### steps list
|
|
|
|
```
|
|
steps list [--verbose] [--output text|json]
|
|
```
|
|
|
|
Show all steps with their indices, status, and modification state.
|
|
|
|
**Output format:**
|
|
```
|
|
Steps:
|
|
✓ 1. Checkout uses actions/checkout@v4
|
|
✓ 2. Setup Node uses actions/setup-node@v4
|
|
▶ 3. Install deps run npm ci
|
|
4. Run tests [MODIFIED] run npm test
|
|
5. Build [ADDED] run npm run build
|
|
6. Deploy run ./deploy.sh
|
|
|
|
Legend: ✓ = completed, ▶ = current/paused, [ADDED] = new, [MODIFIED] = edited
|
|
```
|
|
|
|
**JSON output (`steps list --output json`):**
|
|
```json
|
|
{
|
|
"Success": true,
|
|
"Result": [
|
|
{"index": 1, "name": "Checkout", "type": "uses", "typeDetail": "actions/checkout@v4", "status": "completed"},
|
|
{"index": 2, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "completed"},
|
|
{"index": 3, "name": "Install deps", "type": "run", "typeDetail": "npm ci", "status": "current"},
|
|
{"index": 4, "name": "Run tests", "type": "run", "typeDetail": "npm test", "status": "pending", "change": "MODIFIED"},
|
|
{"index": 5, "name": "Build", "type": "run", "typeDetail": "npm run build", "status": "pending", "change": "ADDED"}
|
|
]
|
|
}
|
|
```
|
|
|
|
### steps add
|
|
|
|
**Run step:**
|
|
```
|
|
steps add run "<script>" [options]
|
|
|
|
Options:
|
|
--name "<name>" Display name
|
|
--shell <shell> Shell (bash, sh, pwsh, etc.)
|
|
--working-directory <path> Working directory
|
|
--if "<condition>" Condition expression
|
|
--env KEY=value Environment variable (repeatable)
|
|
--continue-on-error Don't fail job on step failure
|
|
--timeout <minutes> Step timeout
|
|
--at <index> Insert at position
|
|
--after <index> Insert after step
|
|
--before <index> Insert before step
|
|
--first Insert at first pending position
|
|
--last Insert at end (default)
|
|
--output text|json Response format (default: text)
|
|
```
|
|
|
|
**Uses step:**
|
|
```
|
|
steps add uses <action> [options]
|
|
|
|
Options:
|
|
--name "<name>" Display name
|
|
--with key=value Input parameter (repeatable)
|
|
--if "<condition>" Condition expression
|
|
--env KEY=value Environment variable (repeatable)
|
|
--continue-on-error Don't fail job on step failure
|
|
--timeout <minutes> Step timeout
|
|
[position options same as run]
|
|
--output text|json Response format (default: text)
|
|
```
|
|
|
|
**Examples:**
|
|
```bash
|
|
# Human usage (text output)
|
|
steps add run "npm test" --name "Run Tests" --after 3
|
|
|
|
# Browser extension (JSON output)
|
|
steps add uses actions/setup-node@v4 --with node-version=20 --output json
|
|
```
|
|
|
|
**JSON response (`--output json`):**
|
|
```json
|
|
{
|
|
"Success": true,
|
|
"Message": "Step added at position 4",
|
|
"Result": {"index": 4, "name": "Setup Node", "type": "uses", "typeDetail": "actions/setup-node@v4", "status": "pending", "change": "ADDED"}
|
|
}
|
|
```
|
|
|
|
**Position options:**
|
|
- `--at 3` — Insert at position 3
|
|
- `--after 2` — Insert after step 2
|
|
- `--before 4` — Insert before step 4
|
|
- `--first` — Insert at first pending position
|
|
- `--last` — Insert at end (default if omitted)
|
|
|
|
### steps edit
|
|
|
|
```
|
|
steps edit <index> [modifications]
|
|
|
|
Modifications:
|
|
--name "<name>" Change display name
|
|
--script "<script>" Change script (run only)
|
|
--action "<action>" Change action (uses only)
|
|
--shell <shell> Change shell (run only)
|
|
--working-directory <path> Change working directory
|
|
--if "<condition>" Change condition
|
|
--with key=value Set/update input (uses only)
|
|
--env KEY=value Set/update env var
|
|
--remove-with <key> Remove input
|
|
--remove-env <KEY> Remove env var
|
|
--continue-on-error Enable continue-on-error
|
|
--no-continue-on-error Disable continue-on-error
|
|
--timeout <minutes> Change timeout
|
|
--output text|json Response format (default: text)
|
|
```
|
|
|
|
**Examples:**
|
|
```bash
|
|
# Human usage
|
|
steps edit 4 --script "npm run test:ci" --name "CI Tests"
|
|
|
|
# Browser extension
|
|
steps edit 4 --script "npm run test:ci" --output json
|
|
```
|
|
|
|
**JSON response (`--output json`):**
|
|
```json
|
|
{
|
|
"Success": true,
|
|
"Message": "Step 4 updated",
|
|
"Result": {"index": 4, "name": "CI Tests", "type": "run", "typeDetail": "npm run test:ci", "status": "pending", "change": "MODIFIED"}
|
|
}
|
|
```
|
|
|
|
### steps remove
|
|
|
|
```
|
|
steps remove <index> [--output text|json]
|
|
```
|
|
|
|
**Examples:**
|
|
```bash
|
|
steps remove 5
|
|
steps remove 5 --output json
|
|
```
|
|
|
|
**JSON response (`--output json`):**
|
|
```json
|
|
{"Success": true, "Message": "Step 5 removed"}
|
|
```
|
|
|
|
### steps move
|
|
|
|
```
|
|
steps move <from> <position> [--output text|json]
|
|
|
|
Position (one required):
|
|
--to <index> Move to position
|
|
--after <index> Move after step
|
|
--before <index> Move before step
|
|
--first Move to first pending position
|
|
--last Move to end
|
|
```
|
|
|
|
**Examples:**
|
|
```bash
|
|
steps move 5 --after 2
|
|
steps move 5 --after 2 --output json
|
|
```
|
|
|
|
**JSON response (`--output json`):**
|
|
```json
|
|
{"Success": true, "Message": "Step moved to position 3"}
|
|
```
|
|
|
|
### steps export
|
|
|
|
```
|
|
steps export [--changes-only] [--with-comments] [--output text|json]
|
|
```
|
|
|
|
**Examples:**
|
|
```bash
|
|
steps export --with-comments
|
|
steps export --changes-only --output json
|
|
```
|
|
|
|
**JSON response (`--output json`):**
|
|
```json
|
|
{
|
|
"Success": true,
|
|
"Result": {
|
|
"yaml": "steps:\n - name: Checkout\n uses: actions/checkout@v4\n...",
|
|
"totalSteps": 5,
|
|
"addedCount": 1,
|
|
"modifiedCount": 1
|
|
}
|
|
}
|
|
```
|
|
!step edit <index> [modifications]
|
|
|
|
Modifications:
|
|
--name "<name>" Change display name
|
|
--script "<script>" Change script (run only)
|
|
--action "<action>" Change action (uses only)
|
|
--shell <shell> Change shell (run only)
|
|
--working-directory <path> Change working directory
|
|
--if "<condition>" Change condition
|
|
--with key=value Set/update input (uses only)
|
|
--env KEY=value Set/update env var
|
|
--remove-with <key> Remove input
|
|
--remove-env <KEY> Remove env var
|
|
--continue-on-error Enable continue-on-error
|
|
--no-continue-on-error Disable continue-on-error
|
|
--timeout <minutes> Change timeout
|
|
```
|
|
|
|
**JSON:**
|
|
```json
|
|
{
|
|
"cmd": "step.edit",
|
|
"index": 4,
|
|
"script": "npm run test:ci",
|
|
"name": "CI Tests",
|
|
"with": {"node-version": "22"},
|
|
"removeWith": ["cache"],
|
|
"env": {"CI": "true"},
|
|
"removeEnv": ["DEBUG"]
|
|
}
|
|
```
|
|
|
|
### !step remove
|
|
|
|
```
|
|
!step remove <index>
|
|
```
|
|
|
|
**JSON:**
|
|
```json
|
|
{"cmd": "step.remove", "index": 5}
|
|
```
|
|
|
|
### !step move
|
|
|
|
```
|
|
!step move <from> <position>
|
|
|
|
Position (one required):
|
|
--to <index> Move to position
|
|
--after <index> Move after step
|
|
--before <index> Move before step
|
|
--first Move to first pending position
|
|
--last Move to end
|
|
```
|
|
|
|
**JSON:**
|
|
```json
|
|
{"cmd": "step.move", "from": 5, "position": {"after": 2}}
|
|
```
|
|
|
|
### !step export
|
|
|
|
```
|
|
!step export [--changes-only] [--with-comments]
|
|
```
|
|
|
|
**JSON:**
|
|
```json
|
|
{"cmd": "step.export", "changesOnly": false, "withComments": true}
|
|
```
|
|
|
|
---
|
|
|
|
## Validation Rules
|
|
|
|
1. **Index bounds**: Must be 1 ≤ index ≤ total_steps
|
|
2. **Completed steps**: Cannot edit, remove, or move completed steps
|
|
3. **Current step**: Cannot remove or move the currently executing step (can edit for next run if step-back)
|
|
4. **Position conflicts**: `--at`, `--after`, `--before`, `--first`, `--last` are mutually exclusive
|
|
5. **Type-specific options**: `--script`, `--shell`, `--working-directory` only for run; `--action`, `--with` only for uses
|
|
6. **Required values**: `--at`, `--after`, `--before`, `--to` require an index value
|
|
|
|
---
|
|
|
|
## Error Codes
|
|
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| `INVALID_INDEX` | Index out of range or refers to completed/current step |
|
|
| `INVALID_COMMAND` | Unknown command |
|
|
| `INVALID_OPTION` | Unknown or conflicting options |
|
|
| `INVALID_TYPE` | Invalid step type (not "run" or "uses") |
|
|
| `ACTION_DOWNLOAD_FAILED` | Failed to download action for uses step |
|
|
| `PARSE_ERROR` | Failed to parse command/JSON |
|
|
|
|
---
|
|
|
|
## Future Commands (Reserved)
|
|
|
|
These command names are reserved for future implementation:
|
|
|
|
```
|
|
steps duplicate <index> # Clone a step
|
|
steps enable <index> # Re-enable a disabled step
|
|
steps disable <index> # Skip step without removing
|
|
steps inspect <index> # Show detailed step info
|
|
steps reset <index> # Revert modifications
|
|
steps import # Add steps from YAML
|
|
```
|
|
|
|
---
|
|
|
|
## Notes for Production
|
|
|
|
1. **Action Restrictions:** Before production, must integrate with organization/enterprise action policies (allowed lists, verified creators, etc.)
|
|
|
|
2. **Security:** Dynamic step execution has security implications - ensure proper sandboxing and audit logging
|
|
|
|
3. **Persistence:** Consider whether modifications should persist across step-back operations
|
|
|
|
4. **Undo:** Consider adding `!step undo` for reverting last operation
|