Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions DevOps/Actions/PrCommentAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using DevOps.Options;
using DevOps.Services;
using DevOps.Utils;

namespace DevOps.Actions;

internal static class PrCommentAction
{
internal static async Task<int> Execute(PrCommentOptions opts, CancellationToken ct)
{
try
{
var pr = await HttpService.GetPullRequest(opts.Id, ct);
var repo = pr.Repository?.Name;
var project = pr.Repository?.Project?.Name;
if (string.IsNullOrEmpty(repo) || string.IsNullOrEmpty(project))
{
ConsoleHelper.WriteError($"Could not resolve the repository for pull request {opts.Id}.");
return 1;
}

await HttpService.AddPullRequestComment(project, repo, opts.Id, opts.Message, ct);
ConsoleHelper.WriteSuccess($"Comment added to pull request #{opts.Id}.");
return 0;
}
catch (Exception ex)
{
ConsoleHelper.WriteError($"Error: {ex.Message}");
return 1;
}
}
}
13 changes: 13 additions & 0 deletions DevOps/Options/PrCommentOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using CommandLine;

namespace DevOps.Options;

[Verb("pr-comment", HelpText = "Add a comment to a pull request.")]
public class PrCommentOptions
{
[Option('i', "id", Required = true, HelpText = "Pull request ID.")]
public int Id { get; set; }

[Option('m', "message", Required = true, HelpText = "Comment text.")]
public string Message { get; set; }
}
4 changes: 3 additions & 1 deletion DevOps/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
typeof(PrOpenOptions),
typeof(PrVoteOptions),
typeof(PrAbandonOptions),
typeof(PrCompleteOptions)
typeof(PrCompleteOptions),
typeof(PrCommentOptions)
};

var result = Parser.Default.ParseArguments(args, optionTypes);
Expand Down Expand Up @@ -58,6 +59,7 @@ await result.MapResult(
PrVoteOptions o => PrVoteAction.Execute(o, cts.Token),
PrAbandonOptions o => PrAbandonAction.Execute(o, cts.Token),
PrCompleteOptions o => PrCompleteAction.Execute(o, cts.Token),
PrCommentOptions o => PrCommentAction.Execute(o, cts.Token),
_ => Task.FromResult(1)
},
_ => Task.FromResult(1)
Expand Down
6 changes: 6 additions & 0 deletions DevOps/Responses/AzureDevOpsResponses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ public class PullRequestProject
public string Name { get; set; }
}

public class PullRequestThreadResponse
{
[JsonProperty("id")]
public int Id { get; set; }
}

public class IdentityListResponse
{
[JsonProperty("value")]
Expand Down
22 changes: 22 additions & 0 deletions DevOps/Services/HttpService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,28 @@ public static async Task VotePullRequest(string project, string repo, int pullRe
throw new Exception($"Failed to vote on pull request {pullRequestId}. Status: {response.StatusCode}. {response.Content}");
}

/// <summary>Adds a top-level comment thread to a pull request, returning the created thread id.</summary>
public static async Task<int> AddPullRequestComment(string project, string repo, int pullRequestId, string content, CancellationToken cancellationToken = default)
{
using var client = await CreateClientAsync(cancellationToken);
var request = new RestRequest($"{project}/_apis/git/repositories/{Uri.EscapeDataString(repo)}/pullrequests/{pullRequestId}/threads", Method.Post);
request.AddQueryParameter("api-version", API_VERSION);

var body = new
{
comments = new[] { new { parentCommentId = 0, content, commentType = 1 } },
status = 1
};
request.AddStringBody(JsonConvert.SerializeObject(body), DataFormat.Json);

var response = await client.ExecuteAsync(request, cancellationToken);

if (!response.IsSuccessStatusCode)
throw new Exception($"Failed to comment on pull request {pullRequestId}. Status: {response.StatusCode}. {response.Content}");

return JsonConvert.DeserializeObject<PullRequestThreadResponse>(response.Content)?.Id ?? 0;
}

/// <summary>Sets a pull request status (e.g. "abandoned"), returning the updated PR.</summary>
public static async Task<PullRequestResponse> SetPullRequestStatus(string project, string repo, int pullRequestId, string status, CancellationToken cancellationToken = default)
{
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,21 @@ devops pr-vote -i 123 -v reset

---

### `pr-comment` — Add a comment to a pull request

Posts a top-level comment thread. Works by PR ID; the repository is resolved automatically.

```powershell
devops pr-comment -i 123 -m "Looks good, one nit on the naming."
```

| Option | Alias | Description |
|---|---|---|
| `--id` | `-i` | Pull request ID (required) |
| `--message` | `-m` | Comment text (required) |

---

### `pr-abandon` — Abandon a pull request

```powershell
Expand Down
Loading