# 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 (!step list, !step add run, !step edit, !step remove, !step move) - [x] **Chunk 6:** Action Download Integration (!step add uses) - [x] **Chunk 7:** Export Command (!step export) - [x] **Chunk 8:** JSON API for Browser Extension - [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 both REPL commands and JSON API for different clients - **Non-goal:** Full workflow file reconstruction (steps section only) - **Non-goal:** Production action restriction enforcement (noted for later) ## Command API Specification ### Grammar ``` !step [target] [options] ``` ### 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 | |---------|---------|---------| | `!step list` | Show all steps | `!step list --verbose` | | `!step add` | Add new step | `!step add run "npm test" --after 3` | | `!step edit` | Modify step | `!step edit 4 --script "npm run test:ci"` | | `!step remove` | Delete step | `!step remove 5` | | `!step move` | Reorder step | `!step move 5 --after 2` | | `!step export` | Generate YAML | `!step export --with-comments` | ### Position Modifiers For `!step add` and `!step move`: - `--at ` — Insert at specific position - `--after ` — Insert after step - `--before ` — 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); // "!step add run \"echo hello\" --after 3" bool IsStepCommand(string input); // Starts with "!step" or is JSON with cmd:"step.*" } public abstract class StepCommand { } 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 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 env = null, string condition = null, bool continueOnError = false, int? timeoutMinutes = null); Pipelines.ActionStep CreateUsesStep( string actionReference, // "owner/repo@ref" string name = null, Dictionary with = null, Dictionary 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(); 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 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 edit); // Change tracking IReadOnlyList 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 `!step list`, `!step add run`, `!step edit`, `!step remove`, `!step 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 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 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 (!step add uses) **Goal:** Support `!step 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(); 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 (!step 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 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: JSON API for Browser Extension **Goal:** Add JSON command support for programmatic access. **Files to modify:** - `src/Runner.Worker/Dap/StepCommands/StepCommandParser.cs` - `src/Runner.Worker/Dap/DapDebugSession.cs` **Details:** 1. **Detect JSON input:** ```csharp public bool IsStepCommand(string input) { var trimmed = input.Trim(); return trimmed.StartsWith("!step") || (trimmed.StartsWith("{") && trimmed.Contains("\"cmd\"") && trimmed.Contains("\"step.")); } ``` 2. **Parse JSON commands:** ```csharp public StepCommand Parse(string input) { var trimmed = input.Trim(); if (trimmed.StartsWith("{")) return ParseJsonCommand(trimmed); else return ParseReplCommand(trimmed); } private StepCommand ParseJsonCommand(string json) { var obj = JObject.Parse(json); var cmd = obj["cmd"]?.ToString(); return cmd switch { "step.list" => new ListCommand { Verbose = obj["verbose"]?.Value() ?? false }, "step.add" => ParseJsonAddCommand(obj), "step.edit" => ParseJsonEditCommand(obj), "step.remove" => new RemoveCommand { Index = obj["index"].Value() }, "step.move" => ParseJsonMoveCommand(obj), "step.export" => new ExportCommand { ChangesOnly = obj["changesOnly"]?.Value() ?? false, WithComments = obj["withComments"]?.Value() ?? false }, _ => throw new StepCommandException($"Unknown command: {cmd}") }; } ``` 3. **JSON response format:** ```csharp // For JSON input, return structured JSON response if (wasJsonInput) { return CreateSuccessResponse(new EvaluateResponseBody { Result = JsonConvert.SerializeObject(result), Type = "json" }); } ``` **Testing:** - All commands via JSON - Verify JSON responses are parseable - Test error responses **Estimated effort:** Small-medium --- ### 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
Steps
``` 2. **Step List Rendering:** ```javascript function renderSteps(steps) { return steps.map(step => `
${getStatusIcon(step.status)} ${step.index}. ${step.name} ${step.change ? `[${step.change}]` : ''} ${step.type} ${step.status === 'pending' ? renderStepActions(step) : ''}
`).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 JSON API:** ```javascript async function addStep(type, options) { const cmd = { cmd: 'step.add', type, ...options }; const response = await sendEvaluate(JSON.stringify(cmd)); refreshStepList(); } ``` **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 | | `content.js` | 9 | Steps panel, dialogs, export modal | | `content.css` | 9 | Styling for new UI elements | | `background.js` | 9 | Helper functions if needed | --- ## Command API Full Reference ### !step list ``` !step list [--verbose] ``` 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:** ```json {"cmd": "step.list", "verbose": false} ``` ### !step add **Run step:** ``` !step add run "