11using System . Diagnostics ;
22using System . Globalization ;
3+ using System . Net . Http . Json ;
4+ using System . Text . Json ;
35using System . Text . RegularExpressions ;
46using Microsoft . Extensions . Configuration ;
57using Microsoft . Extensions . Logging ;
1416
1517namespace 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