Skip to content

Commit e4e03cc

Browse files
committed
Create public announcement discussion on release
1 parent 0d0a6fb commit e4e03cc

File tree

1 file changed

+140
-4
lines changed

1 file changed

+140
-4
lines changed

src/Web/Webhook.cs

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Diagnostics;
22
using System.Globalization;
3+
using System.Net.Http.Json;
4+
using System.Text.Json;
35
using System.Text.RegularExpressions;
46
using Microsoft.Extensions.Configuration;
57
using Microsoft.Extensions.Logging;
@@ -14,7 +16,7 @@
1416

1517
namespace Devlooped.Sponsors;
1618

17-
public partial class Webhook(SponsorsManager manager, SponsoredIssues issues, IConfiguration config, IGitHubClient github, IPushover notifier, ILogger<Webhook> logger) : WebhookEventProcessor
19+
public partial class Webhook(SponsorsManager manager, SponsoredIssues issues, IConfiguration config, IGitHubClient github, IPushover notifier, ILogger<Webhook> logger, IHttpClientFactory httpFactory) : WebhookEventProcessor
1820
{
1921
static readonly ActivitySource tracer = ActivityTracer.Source;
2022

@@ -115,15 +117,16 @@ protected override async ValueTask ProcessReleaseWebhookAsync(WebhookHeaders hea
115117
{after.Trim()}
116118
""";
117119

120+
var repo = payload.Repository;
121+
118122
if (!string.Equals(newBody, body, StringComparison.Ordinal))
119123
{
120-
var repo = payload.Repository;
121124
if (repo is not null)
122125
{
123126
if (payload.Release.Draft)
124127
{
125128
await github.Repository.Release.Delete(repo.Owner.Login, repo.Name, payload.Release.Id);
126-
await github.Repository.Release.Create(repo.Owner.Login, repo.Name,
129+
var release = await github.Repository.Release.Create(repo.Owner.Login, repo.Name,
127130
new NewRelease(payload.Release.TagName)
128131
{
129132
Name = payload.Release.Name,
@@ -132,14 +135,17 @@ await github.Repository.Release.Create(repo.Owner.Login, repo.Name,
132135
Prerelease = payload.Release.Prerelease,
133136
TargetCommitish = payload.Release.TargetCommitish
134137
});
138+
139+
await CreateReleaseDiscussion(release, newBody, repo, cancellationToken);
135140
}
136141
else
137142
{
138-
await github.Repository.Release.Edit(repo.Owner.Login, repo.Name, payload.Release.Id,
143+
var release = await github.Repository.Release.Edit(repo.Owner.Login, repo.Name, payload.Release.Id,
139144
new ReleaseUpdate
140145
{
141146
Body = newBody
142147
});
148+
await CreateReleaseDiscussion(release, newBody, repo, cancellationToken);
143149
}
144150
}
145151
}
@@ -154,6 +160,136 @@ await github.Repository.Release.Edit(repo.Owner.Login, repo.Name, payload.Releas
154160
await base.ProcessReleaseWebhookAsync(headers, payload, action);
155161
}
156162

163+
async Task CreateReleaseDiscussion(Octokit.Release release, string newBody, Octokit.Webhooks.Models.Repository repo, CancellationToken cancellationToken)
164+
{
165+
if (config["SponsorLink:Account"] is string account)
166+
{
167+
try
168+
{
169+
var discussionTitle = $"New release {repo.Owner.Login}/{repo.Name}@{release.TagName}";
170+
var discussionBody = $"{newBody}\n\n---\n\n🔗 [View Release]({release.HtmlUrl})";
171+
172+
await CreateDiscussionAsync(account, ".github", discussionTitle, discussionBody, cancellationToken);
173+
}
174+
catch (Exception e)
175+
{
176+
// Don't fail the whole webhook if discussion creation fails
177+
logger.LogWarning(e, "Failed to create discussion for release {Release}", release.TagName);
178+
}
179+
}
180+
}
181+
182+
async Task CreateDiscussionAsync(string owner, string repo, string title, string body, CancellationToken cancellationToken)
183+
{
184+
// First, get the repository ID and discussion category ID
185+
var getRepoQuery = """
186+
query($owner: String!, $repo: String!) {
187+
repository(owner: $owner, name: $repo) {
188+
id
189+
discussionCategories(first: 10) {
190+
nodes {
191+
id
192+
name
193+
}
194+
}
195+
}
196+
}
197+
""";
198+
199+
using var httpClient = httpFactory.CreateClient();
200+
201+
// Add authentication header
202+
if (config["GitHub:Token"] is string token)
203+
{
204+
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
205+
httpClient.DefaultRequestHeaders.Add("User-Agent", "SponsorLink-Webhook");
206+
}
207+
208+
var queryResponse = await httpClient.PostAsJsonAsync("https://api.github.com/graphql", new
209+
{
210+
query = getRepoQuery,
211+
variables = new { owner, repo }
212+
}, cancellationToken);
213+
214+
if (!queryResponse.IsSuccessStatusCode)
215+
{
216+
logger.LogWarning("Failed to get repository info for discussion creation: {Status}", queryResponse.StatusCode);
217+
return;
218+
}
219+
220+
var queryResult = await queryResponse.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken: cancellationToken);
221+
var repositoryId = queryResult?.RootElement
222+
.GetProperty("data")
223+
.GetProperty("repository")
224+
.GetProperty("id")
225+
.GetString();
226+
227+
// Find the "Announcements" category
228+
var categoryId = queryResult?.RootElement
229+
.GetProperty("data")
230+
.GetProperty("repository")
231+
.GetProperty("discussionCategories")
232+
.GetProperty("nodes")
233+
.EnumerateArray()
234+
.FirstOrDefault(node =>
235+
node.TryGetProperty("name", out var nameProperty) &&
236+
nameProperty.GetString() == "Announcements")
237+
.GetProperty("id")
238+
.GetString();
239+
240+
if (string.IsNullOrEmpty(repositoryId) || string.IsNullOrEmpty(categoryId))
241+
{
242+
logger.LogWarning("Could not find repository or Announcements category for {Owner}/{Repo}", owner, repo);
243+
return;
244+
}
245+
246+
// Create the discussion
247+
var createDiscussionMutation = """
248+
mutation($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
249+
createDiscussion(input: {
250+
repositoryId: $repositoryId,
251+
categoryId: $categoryId,
252+
title: $title,
253+
body: $body
254+
}) {
255+
discussion {
256+
id
257+
url
258+
}
259+
}
260+
}
261+
""";
262+
263+
var mutationResponse = await httpClient.PostAsJsonAsync("https://api.github.com/graphql", new
264+
{
265+
query = createDiscussionMutation,
266+
variables = new
267+
{
268+
repositoryId,
269+
categoryId,
270+
title,
271+
body
272+
}
273+
}, cancellationToken);
274+
275+
if (mutationResponse.IsSuccessStatusCode)
276+
{
277+
var mutationResult = await mutationResponse.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken: cancellationToken);
278+
var discussionUrl = mutationResult?.RootElement
279+
.GetProperty("data")
280+
.GetProperty("createDiscussion")
281+
.GetProperty("discussion")
282+
.GetProperty("url")
283+
.GetString();
284+
285+
logger.LogInformation("Created discussion for release: {DiscussionUrl}", discussionUrl);
286+
}
287+
else
288+
{
289+
logger.LogWarning("Failed to create discussion: {Status}", mutationResponse.StatusCode);
290+
}
291+
}
292+
157293
protected override async ValueTask ProcessIssueCommentWebhookAsync(WebhookHeaders headers, IssueCommentEvent payload, IssueCommentAction action, CancellationToken cancellationToken = default)
158294
{
159295
if (await issues.UpdateBacked(github, payload.Repository?.Id, (int)payload.Issue.Number) is null)

0 commit comments

Comments
 (0)