Skip to content

Commit 8b5ebf7

Browse files
calin-lupas_dsamcapscalin-lupas_dsamcaps
authored andcommitted
Enhance webhook and issue request handling
- Updated `GitHubWebhookRepository` to save issue numbers and retrieve webhooks by issue. - Modified `IssueRequestRepository` to interact with webhooks and include an optional issue URL. - Improved `DevExIssuesEventProcessorService` to save webhooks with issue numbers and create issue requests as needed. - Adjusted `DevExPullRequestEventProcessorService` to extract and save issue numbers from pull request titles. - Added `IssueRequestRepository` dependency to `RepositoryService` and `TeamService` for better issue request management. - Enhanced `GitHubWebhookEntity` and `IssueRequestEntity` models with new properties for issue URL and number.
1 parent aaf8c38 commit 8b5ebf7

9 files changed

Lines changed: 138 additions & 20 deletions

src/DevExcelerateApi/Data/GitHubWebhookRepository.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DevExcelerateApi.Models;
22
using Octokit.Webhooks;
3+
using Octokit.Webhooks.Events;
34
using System.Text.Json;
45

56
namespace DevExcelerateApi.Data
@@ -10,8 +11,9 @@ namespace DevExcelerateApi.Data
1011
/// <param name="storageContext"></param>
1112
public class GitHubWebhookRepository(IStorageContext<GitHubWebhookEntity> storageContext)
1213
: Repository<GitHubWebhookEntity>(storageContext)
13-
{
14-
public Task SaveWebhookAsync(WebhookHeaders headers, WebhookEvent webhookEvent)
14+
{
15+
16+
public Task SaveWebhookAsync(WebhookHeaders headers, WebhookEvent webhookEvent, long? issueNumber)
1517
{
1618
var webhookEntity = new GitHubWebhookEntity
1719
{
@@ -20,10 +22,23 @@ public Task SaveWebhookAsync(WebhookHeaders headers, WebhookEvent webhookEvent)
2022
GitHubEvent = $"{headers?.Event}.{webhookEvent?.Action}",
2123
SenderLogin = webhookEvent?.Sender?.Login,
2224
Headers = JsonSerializer.Serialize(headers),
23-
Payload = JsonSerializer.Serialize(webhookEvent)
25+
Payload = JsonSerializer.Serialize(webhookEvent),
26+
RepositoryName = webhookEvent?.Repository?.Name,
27+
IssueNumber = issueNumber
2428
};
2529

2630
return CreateAsync(webhookEntity);
31+
}
32+
33+
/// <summary>
34+
/// Gets all webhooks associated with a specific issue request.
35+
/// </summary>
36+
/// <param name="repositoryName">The repository name</param>
37+
/// <param name="issueNumber">The issue number</param>
38+
/// <returns>Collection of webhooks for the issue</returns>
39+
public async Task<IEnumerable<GitHubWebhookEntity>> GetWebhooksByIssueAsync(string repositoryName, long issueNumber)
40+
{
41+
return await StorageContext.QueryEntitiesAsync(w => w.RepositoryName == repositoryName && w.IssueNumber == issueNumber);
2742
}
2843
}
2944
}

src/DevExcelerateApi/Data/IssueRequestRepository.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ namespace DevExcelerateApi.Data
77
/// Demonstrates how easy it is to create repositories for new entity types.
88
/// </summary>
99
/// <param name="storageContext">The storage context for IssueRequestEntity</param>
10-
public class IssueRequestRepository(IStorageContext<IssueRequestEntity> storageContext)
10+
public class IssueRequestRepository(
11+
IStorageContext<IssueRequestEntity> storageContext,
12+
GitHubWebhookRepository? webhookRepository = null)
1113
: Repository<IssueRequestEntity>(storageContext)
1214
{
15+
private readonly GitHubWebhookRepository? _webhookRepository = webhookRepository;
16+
1317
/// <summary>
1418
/// Saves an issue request with additional business logic.
1519
/// </summary>
@@ -22,14 +26,16 @@ public class IssueRequestRepository(IStorageContext<IssueRequestEntity> storageC
2226
public async Task<IssueRequestEntity> CreateIssueRequestAsync(
2327
string repositoryName,
2428
long issueNumber,
25-
string title,
29+
string? issueUrl,
30+
string? title,
2631
string? body = null,
2732
string? createdBy = null)
2833
{
2934
var issueRequest = new IssueRequestEntity
3035
{
3136
RepositoryName = repositoryName,
3237
IssueNumber = issueNumber,
38+
IssueUrl = issueUrl,
3339
Title = title,
3440
Body = body,
3541
CreatedBy = createdBy,
@@ -88,5 +94,19 @@ public async Task<IEnumerable<IssueRequestEntity>> FindByDateRangeAsync(DateTime
8894
return await StorageContext.QueryEntitiesAsync(entity =>
8995
entity.CreatedOn >= startDate && entity.CreatedOn <= endDate);
9096
}
97+
98+
/// <summary>
99+
/// Gets webhooks for an issue request by repository name and issue number.
100+
/// </summary>
101+
/// <param name="repositoryName">The repository name</param>
102+
/// <param name="issueNumber">The issue number</param>
103+
/// <returns>Collection of webhooks for the issue</returns>
104+
public async Task<IEnumerable<GitHubWebhookEntity>> GetWebhooksByIssueAsync(string repositoryName, long issueNumber)
105+
{
106+
if (_webhookRepository == null)
107+
return Enumerable.Empty<GitHubWebhookEntity>();
108+
109+
return await _webhookRepository.GetWebhooksByIssueAsync(repositoryName, issueNumber);
110+
}
91111
}
92112
}

src/DevExcelerateApi/Models/GitHubWebhookEntity.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,21 @@ public class GitHubWebhookEntity : IStorageEntity
6767
/// <summary>
6868
/// The Webhook request headers.
6969
/// </summary>
70-
public string? Headers { get; set; }
71-
70+
public string? Headers { get; set; }
71+
7272
/// <summary>
7373
/// The Webhook request body.
7474
/// </summary>
7575
public string? Payload { get; set; }
76+
77+
/// <summary>
78+
/// The repository name associated with this webhook.
79+
/// </summary>
80+
public string? RepositoryName { get; set; }
81+
82+
/// <summary>
83+
/// The issue number associated with this webhook (for issue-related events).
84+
/// </summary>
85+
public long? IssueNumber { get; set; }
7686
}
7787
}

src/DevExcelerateApi/Models/IssueRequestEntity.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public class IssueRequestEntity : IStorageEntity
4141
/// </summary>
4242
public long IssueNumber { get; set; }
4343

44+
/// <summary>
45+
///
46+
/// </summary>
47+
public string? IssueUrl { get; set; }
48+
4449
/// <summary>
4550
/// The title of the issue.
4651
/// </summary>

src/DevExcelerateApi/Services/DevExIssuesEventProcessorService.cs

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using DevExcelerateApi.Core.Extensions;
22
using DevExcelerateApi.Data;
33
using DevExcelerateApi.Models;
4+
using Microsoft.Azure.Cosmos.Linq;
45
using Microsoft.FeatureManagement;
56
using Octokit.Webhooks;
67
using Octokit.Webhooks.Events;
@@ -10,13 +11,14 @@
1011
using static DevExcelerateApi.Parsers.GitHubIssueFormParser;
1112

1213
namespace DevExcelerateApi.Services
13-
{
14+
{
1415
public class DevExIssuesEventProcessorService(
1516
ILogger<DevExIssuesEventProcessorService> logger,
1617
IRepositoryService repositoryService,
1718
ITeamService teamService,
1819
IIssueService issueService,
1920
GitHubWebhookRepository gitHubWebhookRepository,
21+
IssueRequestRepository issueRequestRepository,
2022
ILocalizationService localizationService,
2123
IVariantFeatureManager featureManager)
2224
: IDevExIssuesEventProcessorService
@@ -26,15 +28,21 @@ public class DevExIssuesEventProcessorService(
2628
private readonly ITeamService _teamService = teamService;
2729
private readonly IIssueService _issueService = issueService;
2830
private readonly GitHubWebhookRepository _gitHubWebhookRepository = gitHubWebhookRepository;
31+
private readonly IssueRequestRepository _issueRequestRepository = issueRequestRepository;
2932
private readonly ILocalizationService _localizationService = localizationService;
30-
private readonly IVariantFeatureManager _featureManager = featureManager;
31-
33+
private readonly IVariantFeatureManager _featureManager = featureManager;
34+
3235
public async Task SaveAndProcessIssueEventAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action)
3336
{
3437
_logger.LogInformation("Starting the process of the Issues event for Issue# {number} with action - {action}...", issuesEvent?.Issue.Number, issuesEvent?.Action);
3538

36-
await _gitHubWebhookRepository.SaveWebhookAsync(headers, issuesEvent!).ConfigureAwait(false);
39+
// 1. Create issue request to group all webhooks related to an IssueNumber#
40+
await CreateIssueRequestIfNeeded(issuesEvent!).ConfigureAwait(false);
41+
42+
// 2. Save the webhook
43+
await _gitHubWebhookRepository.SaveWebhookAsync(headers, issuesEvent!, issuesEvent?.Issue.Number).ConfigureAwait(false);
3744

45+
// 3. Validate issue
3846
switch (action)
3947
{
4048
case IssuesActionValue.Opened:
@@ -204,10 +212,58 @@ private async Task ValidateIssueCommentApproval(string data, IssueCommentEvent i
204212
await _issueService.ChangeIssueLabel([_localizationService.GetLocalizedString(nameof(TicketStatus.APPROVED))], issueCommentEvent!);
205213
}
206214
// Opened -> Rejected
207-
else if ((data?.IsIssueRejected()).GetValueOrDefault() && !(issueCommentEvent?.Issue?.Labels?.Any(i => i.Name.IsIssueRejected())).GetValueOrDefault())
208-
{
215+
else if ((data?.IsIssueRejected()).GetValueOrDefault() && !(issueCommentEvent?.Issue?.Labels?.Any(i => i.Name.IsIssueRejected())).GetValueOrDefault()) {
209216
await _issueService.ChangeIssueLabel([_localizationService.GetLocalizedString(nameof(TicketStatus.REJECTED))], issueCommentEvent!);
210217
}
211218
}
219+
220+
/// <summary>
221+
/// Creates an issue request entity for tracking purposes and links it to related webhooks.
222+
/// This establishes the 1:N relationship between IssueRequestEntity and GitHubWebhookEntity.
223+
/// </summary>
224+
/// <param name="issuesEvent">The GitHub issues event</param>
225+
private async Task CreateIssueRequestIfNeeded(IssuesEvent issuesEvent)
226+
{
227+
try
228+
{
229+
var repositoryName = issuesEvent?.Repository?.Name;
230+
var issueNumber = issuesEvent?.Issue?.Number;
231+
var issueUrl = issuesEvent?.Issue?.HtmlUrl;
232+
var title = issuesEvent?.Issue?.Title;
233+
var body = issuesEvent?.Issue?.Body;
234+
var createdBy = issuesEvent?.Issue?.User?.Login;
235+
236+
if (string.IsNullOrEmpty(repositoryName) || !issueNumber.HasValue || string.IsNullOrEmpty(title))
237+
{
238+
_logger.LogWarning("Cannot create issue request - missing required data: Repository={Repository}, Issue={Issue}, Title={Title}", repositoryName, issueNumber, title);
239+
return;
240+
}
241+
242+
// Check if issue request already exists to avoid duplicates
243+
var existingRequests = await _issueRequestRepository.FindByRepositoryAsync(repositoryName);
244+
var existingRequest = existingRequests.FirstOrDefault(r => r.IssueNumber == issueNumber.Value);
245+
246+
if (existingRequest != null)
247+
{
248+
_logger.LogInformation("Issue request already exists for Repository={Repository}, Issue={Issue}", repositoryName, issueNumber);
249+
return;
250+
}
251+
252+
// Create new issue request
253+
var issueRequest = await _issueRequestRepository.CreateIssueRequestAsync(
254+
repositoryName,
255+
issueNumber.Value,
256+
issueUrl,
257+
title,
258+
body,
259+
createdBy);
260+
261+
_logger.LogInformation("Created issue request {IssueRequestId} for Repository={Repository}, Issue={Issue}", issueRequest.Id, repositoryName, issueNumber);
262+
}
263+
catch (Exception ex)
264+
{
265+
_logger.LogError(ex, "Failed to create issue request for Repository={Repository}, Issue={Issue}", issuesEvent?.Repository?.Name, issuesEvent?.Issue?.Number);
266+
}
267+
}
212268
}
213269
}

src/DevExcelerateApi/Services/DevExPullRequestEventProcessorService.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ public async Task SaveAndProcessPullRequestEventAsync(WebhookHeaders headers, Pu
2323
{
2424
_logger.LogInformation("Starting the process of the PullRequest event for PR# {number} with action - {action}...", pullRequestEvent?.PullRequest.Number, pullRequestEvent?.Action);
2525

26-
await _gitHubWebhookRepository.SaveWebhookAsync(headers, pullRequestEvent!).ConfigureAwait(false);
27-
26+
// 1. Get the issue number from the PR
27+
_ = long.TryParse(pullRequestEvent?.PullRequest?.Title?.Split(Constants.GitHubPullRequestTitleIssueNumber)?.LastOrDefault(), out long issueNumber);
28+
29+
// 2. Save the webhook
30+
await _gitHubWebhookRepository.SaveWebhookAsync(headers, pullRequestEvent!, issueNumber).ConfigureAwait(false);
31+
32+
// 3. Validate PR
2833
switch (action)
2934
{
3035
case PullRequestActionValue.Opened:

src/DevExcelerateApi/Services/RepositoryService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DevExcelerateApi.Core.Extensions;
22
using DevExcelerateApi.Core.Options;
3+
using DevExcelerateApi.Data;
34
using DevExcelerateApi.Helpers;
45
using DevExcelerateApi.Models;
56
using Microsoft.Extensions.Options;
@@ -9,14 +10,15 @@
910
using static DevExcelerateApi.Parsers.GitHubIssueFormParser;
1011

1112
namespace DevExcelerateApi.Services
12-
{
13+
{
1314
public class RepositoryService(
1415
ILogger<RepositoryService> logger,
1516
IGitHubClientFactory gitHubClientFactory,
1617
IIssueService issueService,
1718
IPolicyService policyService,
1819
IOptions<GitHubOptions> gitHubOptions,
19-
ILocalizationService localizationService)
20+
ILocalizationService localizationService,
21+
IssueRequestRepository issueRequestRepository)
2022
: IRepositoryService
2123
{
2224
private readonly ILogger<RepositoryService> _logger = logger;
@@ -25,6 +27,7 @@ public class RepositoryService(
2527
private readonly IPolicyService _policyService = policyService;
2628
private readonly GitHubOptions _gitHubOptions = gitHubOptions.Value;
2729
private readonly ILocalizationService _localizationService = localizationService;
30+
private readonly IssueRequestRepository _issueRequestRepository = issueRequestRepository;
2831

2932
public async Task ValidateRepository(IssuesEvent issuesEvent, bool createPullRequest)
3033
{

src/DevExcelerateApi/Services/TeamService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DevExcelerateApi.Core.Extensions;
22
using DevExcelerateApi.Core.Options;
3+
using DevExcelerateApi.Data;
34
using DevExcelerateApi.Helpers;
45
using DevExcelerateApi.Models;
56
using Microsoft.Extensions.Options;
@@ -9,14 +10,15 @@
910
using static DevExcelerateApi.Parsers.GitHubIssueFormParser;
1011

1112
namespace DevExcelerateApi.Services
12-
{
13+
{
1314
public class TeamService(
1415
ILogger<TeamService> logger,
1516
IGitHubClientFactory gitHubClientFactory,
1617
IIssueService issueService,
1718
IPolicyService policyService,
1819
IOptions<GitHubOptions> gitHubOptions,
19-
ILocalizationService localizationService)
20+
ILocalizationService localizationService,
21+
IssueRequestRepository issueRequestRepository)
2022
: ITeamService
2123
{
2224
private readonly ILogger<TeamService> _logger = logger;
@@ -25,6 +27,7 @@ public class TeamService(
2527
private readonly IPolicyService _policyService = policyService;
2628
private readonly GitHubOptions _gitHubOptions = gitHubOptions.Value;
2729
private readonly ILocalizationService _localizationService = localizationService;
30+
private readonly IssueRequestRepository _issueRequestRepository = issueRequestRepository;
2831

2932
public async Task ValidateTeam(IssuesEvent issuesEvent, bool createPullRequest)
3033
{

src/DevExcelerateApi/appsettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"Type": "",
3232
"FileSystem": {
3333
"FilePath": ""
34-
}, "CosmosDb": {
34+
},
35+
"CosmosDb": {
3536
"Database": "",
3637
// dotnet user-secrets set "DataStore:CosmosDb:ConnectionString" "MY_COSMOS_CONNECTION_STRING"
3738
"ConnectionString": ""

0 commit comments

Comments
 (0)