From dc8368c2d1977d12dce2207c9706ebafacc6a3b5 Mon Sep 17 00:00:00 2001 From: Jonas Lima de Amorim Date: Thu, 23 Jul 2026 18:42:40 -0300 Subject: [PATCH] feat: add delete command for work items (recycle bin) --- DevOps/Actions/DeleteAction.cs | 50 +++++++++++++++++++++++++++++++++ DevOps/Options/DeleteOptions.cs | 16 +++++++++++ DevOps/Program.cs | 2 ++ DevOps/Services/HttpService.cs | 13 +++++++++ README.md | 18 ++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 DevOps/Actions/DeleteAction.cs create mode 100644 DevOps/Options/DeleteOptions.cs diff --git a/DevOps/Actions/DeleteAction.cs b/DevOps/Actions/DeleteAction.cs new file mode 100644 index 0000000..d100bbf --- /dev/null +++ b/DevOps/Actions/DeleteAction.cs @@ -0,0 +1,50 @@ +using DevOps.Options; +using DevOps.Services; +using DevOps.Utils; + +namespace DevOps.Actions; + +internal static class DeleteAction +{ + internal static async Task Execute(DeleteOptions opts, CancellationToken ct) + { + try + { + var project = ConfigService.ResolveProject(opts.Project); + var ids = opts.Ids.ToList(); + + if (!opts.Force) + { + Console.WriteLine($"About to delete {ids.Count} work item(s): {string.Join(", ", ids.Select(i => "#" + i))}"); + Console.Write("They will be moved to the recycle bin. Continue? [y/N] "); + var answer = Console.ReadLine(); + if (!string.Equals(answer, "y", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Aborted."); + return 0; + } + } + + var tasks = ids.Select(async id => + { + try + { + await HttpService.DeleteWorkItem(id, project, ct); + ConsoleHelper.WriteSuccess($"Deleted work item #{id}"); + } + catch (Exception ex) + { + ConsoleHelper.WriteError($"Error on #{id}: {ex.Message}"); + } + }); + + await Task.WhenAll(tasks); + return 0; + } + catch (Exception ex) + { + ConsoleHelper.WriteError($"Error: {ex.Message}"); + return 1; + } + } +} diff --git a/DevOps/Options/DeleteOptions.cs b/DevOps/Options/DeleteOptions.cs new file mode 100644 index 0000000..1d77432 --- /dev/null +++ b/DevOps/Options/DeleteOptions.cs @@ -0,0 +1,16 @@ +using CommandLine; + +namespace DevOps.Options; + +[Verb("delete", HelpText = "Delete one or more work items (moves them to the recycle bin).")] +public class DeleteOptions +{ + [Option('i', "id", Required = true, HelpText = "Work item ID(s). Space-separated after the flag: -i 1 2 3.")] + public IEnumerable Ids { get; set; } + + [Option('p', "project", Required = false, HelpText = "Project name. Uses default if configured.")] + public string Project { get; set; } + + [Option("force", Required = false, HelpText = "Skip the confirmation prompt.")] + public bool Force { get; set; } +} diff --git a/DevOps/Program.cs b/DevOps/Program.cs index dabfe43..1686142 100644 --- a/DevOps/Program.cs +++ b/DevOps/Program.cs @@ -16,6 +16,7 @@ typeof(CreateOptions), typeof(UpdateOptions), typeof(StateOptions), + typeof(DeleteOptions), typeof(CommentOptions), typeof(PipelinesOptions), typeof(RunsOptions), @@ -40,6 +41,7 @@ await result.MapResult( CreateOptions o => CreateAction.Execute(o, cts.Token), UpdateOptions o => UpdateAction.Execute(o, cts.Token), StateOptions o => StateAction.Execute(o, cts.Token), + DeleteOptions o => DeleteAction.Execute(o, cts.Token), CommentOptions o => CommentAction.Execute(o, cts.Token), PipelinesOptions o => PipelinesAction.Execute(o, cts.Token), RunsOptions o => RunsAction.Execute(o, cts.Token), diff --git a/DevOps/Services/HttpService.cs b/DevOps/Services/HttpService.cs index 1ab7443..e1f8a75 100644 --- a/DevOps/Services/HttpService.cs +++ b/DevOps/Services/HttpService.cs @@ -341,4 +341,17 @@ public static async Task UpdateWorkItem(int id, string project return JsonConvert.DeserializeObject(response.Content); } + + /// Deletes a work item, moving it to the project recycle bin (recoverable). + public static async Task DeleteWorkItem(int id, string project, CancellationToken cancellationToken = default) + { + using var client = await CreateClientAsync(cancellationToken); + var request = new RestRequest($"{project}/_apis/wit/workitems/{id}", Method.Delete); + request.AddQueryParameter("api-version", API_VERSION); + + var response = await client.ExecuteAsync(request, cancellationToken); + + if (!response.IsSuccessStatusCode) + throw new Exception($"Failed to delete work item {id}. Status: {response.StatusCode}. {response.Content}"); + } } diff --git a/README.md b/README.md index ac52b66..1dd3c42 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,24 @@ Output: `Work item #1234: To Do -> In Progress` --- +### `delete` — Delete one or more work items + +Moves the work items to the project **recycle bin** (recoverable, not a permanent delete). Prompts for confirmation unless `--force`. Multiple IDs are processed in parallel. + +```powershell +devops delete -i 1234 +devops delete -i 1234 5678 9012 +devops delete -i 1234 --force +``` + +| Option | Alias | Description | +|---|---|---| +| `--id` | `-i` | Work item ID(s) (required). Space-separated: `-i 1 2 3` | +| `--project` | `-p` | Project name (uses default if configured) | +| `--force` | | Skip the confirmation prompt | + +--- + ### `open` — Open a work item in the browser ```powershell