Skip to content

Commit da21908

Browse files
author
Calin Lupas
authored
Merge pull request #26 from DevExcelerate/feature/create-repo-from-pr
#25 #22 Add pull request processing and related enhancements
2 parents add21f3 + 2677ca6 commit da21908

14 files changed

Lines changed: 284 additions & 18 deletions

src/DevExcelerateApi/Core/Extensions/ServiceExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ internal static IServiceCollection AddAppServices(this IServiceCollection servic
9393
services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
9494
services.AddSingleton<IGitHubHelper, GitHubHelper>();
9595
services.AddSingleton<IDevExIssuesEventProcessorService, DevExIssuesEventProcessorService>();
96+
services.AddSingleton<IDevExPullRequestEventProcessorService, DevExPullRequestEventProcessorService>();
9697
services.AddSingleton<WebhookEventProcessor, DevExWebhookEventProcessorService>();
9798
services.AddSingleton<IRepositoryService, RepositoryService>();
9899
services.AddSingleton<IIssueService, IssueService>();

src/DevExcelerateApi/Helpers/GitHubHelper.cs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ public async Task<string> GetFileContentAsync(string owner, string repo, string
5454
throw new InvalidOperationException("GitHub client is not configured.");
5555
}
5656

57+
try
58+
{
59+
await ReferenceExtensions.CreateBranch(_githubClient.Git.Reference, owner, repo, branch, defaultBranchName);
60+
}
61+
catch(ApiValidationException ex)
62+
{
63+
// 422: Reference already exists
64+
if (ex.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
65+
{
66+
// Ignore the exception if the branch already exists
67+
}
68+
else
69+
{
70+
throw;
71+
}
72+
}
73+
5774
try
5875
{
5976
// Check if the file already exists
@@ -67,8 +84,6 @@ public async Task<string> GetFileContentAsync(string owner, string repo, string
6784
}
6885
catch (NotFoundException)
6986
{
70-
await ReferenceExtensions.CreateBranch(_githubClient.Git.Reference, owner, repo, branch, defaultBranchName);
71-
7287
var createFileRequest = new CreateFileRequest($"Adding file {path}", content, branch);
7388

7489
return await _githubClient.Repository.Content.CreateFile(owner, repo, path, createFileRequest);
@@ -105,5 +120,55 @@ public async Task<PullRequest> CreatePullRequestAsync(string owner, string repo,
105120

106121
return await _githubClient.PullRequest.Create(owner, repo, newPullRequest);
107122
}
123+
124+
public async Task<IReadOnlyList<GitHubCommitFile>> GetCommitFilesAsync(string owner, string repo, string commitSha)
125+
{
126+
if (_githubClient == null)
127+
{
128+
throw new InvalidOperationException("GitHub client is not configured.");
129+
}
130+
131+
// Fetch the commit details using the commit SHA
132+
var commit = await _githubClient.Repository.Commit.Get(owner, repo, commitSha);
133+
134+
// Return the list of files associated with the commit
135+
return commit.Files;
136+
}
137+
138+
public async Task<Dictionary<string, string>> GetCommitFilesWithContentAsync(string owner, string repo, string commitSha)
139+
{
140+
if (_githubClient == null)
141+
{
142+
throw new InvalidOperationException("GitHub client is not configured.");
143+
}
144+
145+
// Dictionary to store file paths and their content
146+
var fileContents = new Dictionary<string, string>();
147+
148+
var files = await GetCommitFilesAsync(owner, repo, commitSha);
149+
150+
foreach (var file in files)
151+
{
152+
if (file.Status == "added" || file.Status == "modified") // Only fetch content for added/modified files
153+
{
154+
try
155+
{
156+
// Fetch the raw content of the file
157+
var rawContent = await _githubClient.Repository.Content.GetRawContent(owner, repo, file.Filename);
158+
var contentString = System.Text.Encoding.UTF8.GetString(rawContent);
159+
160+
// Add the file path and content to the dictionary
161+
fileContents.Add(file.Filename, contentString);
162+
}
163+
catch (NotFoundException)
164+
{
165+
// Handle cases where the file content is not accessible
166+
fileContents.Add(file.Filename, "Content not accessible");
167+
}
168+
}
169+
}
170+
171+
return fileContents;
172+
}
108173
}
109174
}

src/DevExcelerateApi/Helpers/IGitHubHelper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ public interface IGitHubHelper
99
Task<string> GetFileContentAsync(string owner, string repo, string path);
1010
Task<RepositoryContentChangeSet?> SaveFileAsync(string owner, string repo, string defaultBranchName, string branch, string path, string content);
1111
Task<PullRequest> CreatePullRequestAsync(string owner, string repo, string headBranch, string baseBranch, string title, string body);
12+
Task<IReadOnlyList<GitHubCommitFile>> GetCommitFilesAsync(string owner, string repo, string commitSha);
13+
Task<Dictionary<string, string>> GetCommitFilesWithContentAsync(string owner, string repo, string commitSha);
1214
}
1315
}

src/DevExcelerateApi/Models/Constants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ public static class Constants
99
public const string GitHubPoliciesFolderPath = ".github/policies/";
1010
public const string GitHubInventoryReposFolderPath = "repositories/";
1111
public const string GitHubDefaultBranch = "main";
12+
public const string GitHubPullRequestTitleIssueNumber = "Issue#";
1213

1314
// Constants for the different types of messages that can be sent
1415
public const string ERROR_REPO_CREATION = "ERROR_REPO_CREATION";
1516
public const string ERROR_REPO_EXISTS = "ERROR_REPO_EXISTS";
1617
public const string ERROR_REPO_TEAM_DOES_NOT_EXISTS = "ERROR_REPO_TEAM_DOES_NOT_EXISTS";
1718
public const string ERROR_REPO_RULESET_DOES_NOT_EXISTS = "ERROR_REPO_RULESET_DOES_NOT_EXISTS";
1819
public const string ERROR_REPO_PR_CREATION = "ERROR_REPO_PR_CREATION";
20+
public const string ERROR_REPO_PR_NO_CHANGES = "ERROR_REPO_PR_NO_CHANGES";
1921
public const string VALIDATION_PASSED = "VALIDATION_PASSED";
2022
public const string ISSUE_APPROVED = "ISSUE_APPROVED";
2123
public const string ISSUE_COMPLETED = "ISSUE_COMPLETED";

src/DevExcelerateApi/Resources/Services.RepositoryService.fr.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126
<data name="ERROR_REPO_PR_CREATION" xml:space="preserve">
127127
<value>Un problème est survenu lors de la tentative de création de la demande de tirage.</value>
128128
</data>
129+
<data name="ERROR_REPO_PR_NO_CHANGES" xml:space="preserve">
130+
<value>La demande de tirage fermée n'a aucun fichier modifié. Fermeture du problème validé associé. Si vous devez mettre à jour la ressource, veuillez créer un nouveau problème. Merci!</value>
131+
</data>
129132
<data name="ERROR_REPO_RULESET_DOES_NOT_EXISTS" xml:space="preserve">
130133
<value>Le set de règles '{0}' n'existe pas.</value>
131134
</data>

src/DevExcelerateApi/Resources/Services.RepositoryService.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@
126126
<data name="ERROR_REPO_PR_CREATION" xml:space="preserve">
127127
<value>An issue occurred while attempting to create the pull request.</value>
128128
</data>
129+
<data name="ERROR_REPO_PR_NO_CHANGES" xml:space="preserve">
130+
<value>The closed pull request has no files changed. Closing the related validated issue. If you need to update the resource, please create a new issue. Thank you!</value>
131+
</data>
129132
<data name="ERROR_REPO_RULESET_DOES_NOT_EXISTS" xml:space="preserve">
130133
<value>The '{0}' ruleset does not exist. </value>
131134
</data>

src/DevExcelerateApi/Services/DevExIssuesEventProcessorService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using DevExcelerateApi.Models;
55
using Microsoft.Extensions.Localization;
66
using Microsoft.Extensions.Options;
7+
using Octokit;
78
using Octokit.Webhooks;
89
using Octokit.Webhooks.Events;
910
using Octokit.Webhooks.Events.IssueComment;
@@ -57,8 +58,6 @@ public async Task SaveAndProcessIssueCommentEventAsync(WebhookHeaders headers, I
5758

5859
await _gitHubWebhookRepository.SaveWebhookAsync(headers, issueCommentEvent!).ConfigureAwait(false);
5960

60-
_logger.LogInformation("GitHub Webhook event saved to database.");
61-
6261
switch (action)
6362
{
6463
case IssueCommentActionValue.Created:
@@ -69,6 +68,7 @@ public async Task SaveAndProcessIssueCommentEventAsync(WebhookHeaders headers, I
6968
_logger.LogInformation("Not supported action for processing the issue comment.");
7069
break;
7170
}
71+
_logger.LogInformation("GitHub Webhook event saved to database and processed for Issue# {number} with action - {action}...", (issueCommentEvent?.Issue.Number), issueCommentEvent?.Action);
7272
}
7373

7474
private async Task ValidateIssueApproval(IssuesEvent issuesEvent)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using DevExcelerateApi.Data;
2+
using Octokit;
3+
using Octokit.Webhooks;
4+
using Octokit.Webhooks.Events;
5+
using Octokit.Webhooks.Events.PullRequest;
6+
using Octokit.Webhooks.Models.PullRequestEvent;
7+
8+
namespace DevExcelerateApi.Services
9+
{
10+
public class DevExPullRequestEventProcessorService(
11+
ILogger<DevExPullRequestEventProcessorService> logger,
12+
IRepositoryService repositoryService,
13+
GitHubWebhookRepository gitHubWebhookRepository)
14+
: IDevExPullRequestEventProcessorService
15+
{
16+
private readonly ILogger<DevExPullRequestEventProcessorService> _logger = logger;
17+
private readonly IRepositoryService _repositoryService = repositoryService;
18+
private readonly GitHubWebhookRepository _gitHubWebhookRepository = gitHubWebhookRepository;
19+
20+
public async Task SaveAndProcessPullRequestEventAsync(WebhookHeaders headers, PullRequestEvent pullRequestEvent, PullRequestAction action)
21+
{
22+
_logger.LogInformation("Starting the process of the PullRequest event for PR# {number} with action - {action}...", (pullRequestEvent?.PullRequest.Number), pullRequestEvent?.Action);
23+
24+
await _gitHubWebhookRepository.SaveWebhookAsync(headers, pullRequestEvent!).ConfigureAwait(false);
25+
26+
switch (action)
27+
{
28+
case PullRequestActionValue.Opened:
29+
case PullRequestActionValue.Reopened:
30+
case PullRequestActionValue.Edited:
31+
case PullRequestActionValue.Labeled:
32+
case PullRequestActionValue.Closed:
33+
await ValidatePullRequestApproval(pullRequestEvent!);
34+
break;
35+
36+
default:
37+
_logger.LogInformation("Not supported action for processing the pull request.");
38+
}
39+
40+
_logger.LogInformation("GitHub Webhook event saved to database and processed for PR# {number} with action - {action}...", (pullRequestEvent?.PullRequest.Number), pullRequestEvent?.Action);
41+
}
42+
43+
private async Task ValidatePullRequestApproval(PullRequestEvent pullRequestEvent)
44+
{
45+
var prNumber = (pullRequestEvent?.PullRequest.Number).GetValueOrDefault();
46+
47+
// Closed & Merged
48+
if (pullRequestEvent?.PullRequest.State == PullRequestState.Closed
49+
&& pullRequestEvent?.PullRequest.MergedAt != null
50+
&& pullRequestEvent?.PullRequest.MergeCommitSha != null)
51+
{
52+
_logger.LogInformation("Pull request #{number} is closed and merged.", prNumber);
53+
54+
await _repositoryService.SaveRepository(pullRequestEvent!);
55+
}
56+
else
57+
{
58+
_logger.LogInformation("Pull request #{number} is still in review.", prNumber);
59+
}
60+
}
61+
}
62+
}

src/DevExcelerateApi/Services/DevExWebhookEventProcessorService.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Octokit.Webhooks.Events;
66
using Octokit.Webhooks.Events.IssueComment;
77
using Octokit.Webhooks.Events.Issues;
8+
using Octokit.Webhooks.Events.PullRequest;
89
using Octokit.Webhooks.Models;
910

1011
namespace DevExcelerateApi.Services
@@ -16,18 +17,21 @@ namespace DevExcelerateApi.Services
1617
public class DevExWebhookEventProcessorService(
1718
ILogger<DevExWebhookEventProcessorService> logger,
1819
IOptions<GitHubOptions> gitHubOptions,
19-
IDevExIssuesEventProcessorService devExIssuesEventProcessorService) : WebhookEventProcessor
20+
IDevExIssuesEventProcessorService devExIssuesEventProcessorService,
21+
IDevExPullRequestEventProcessorService devExPullRequestEventProcessorService) : WebhookEventProcessor
2022
{
2123
private readonly ILogger<DevExWebhookEventProcessorService> _logger = logger;
2224
private readonly GitHubOptions _gitHubOptions = gitHubOptions.Value;
2325
private readonly IDevExIssuesEventProcessorService _devExIssuesEventProcessorService = devExIssuesEventProcessorService;
24-
26+
private readonly IDevExPullRequestEventProcessorService _devExPullRequestEventProcessorService = devExPullRequestEventProcessorService;
27+
2528
private readonly IList<string> _allowedWebhookEvents =
2629
[
2730
WebhookEventType.Issues,
2831
WebhookEventType.IssueComment,
2932
WebhookEventType.Repository,
30-
//WebhookEventType.RepositoryRuleset,
33+
WebhookEventType.RepositoryRuleset,
34+
WebhookEventType.PullRequest
3135
];
3236

3337
private readonly IList<IssuesAction> _allowedIssuesActions =
@@ -39,6 +43,15 @@ public class DevExWebhookEventProcessorService(
3943
IssuesAction.Labeled,
4044
];
4145

46+
private readonly IList<PullRequestAction> _allowedPullRequestActions =
47+
[
48+
PullRequestAction.Opened,
49+
PullRequestAction.Edited,
50+
PullRequestAction.Labeled,
51+
PullRequestAction.Closed,
52+
PullRequestAction.Reopened
53+
];
54+
4255
public override Task ProcessWebhookAsync(IDictionary<string, StringValues> headers, string body)
4356
{
4457
ArgumentNullException.ThrowIfNull(headers);
@@ -73,7 +86,6 @@ protected override Task ProcessIssuesWebhookAsync(WebhookHeaders headers, Issues
7386
return Task.CompletedTask;
7487
}
7588

76-
_logger.LogInformation("--> Processing the Issues event for Issue# {Number}.", issuesEvent?.Issue?.Number);
7789
return _devExIssuesEventProcessorService.SaveAndProcessIssueEventAsync(headers, issuesEvent!, action);
7890
}
7991

@@ -86,8 +98,21 @@ protected override Task ProcessIssueCommentWebhookAsync(WebhookHeaders headers,
8698
return Task.CompletedTask;
8799
}
88100

89-
_logger.LogInformation("--> Processing the Issue Comment event for Issue# {Number}.", issueCommentEvent?.Issue?.Number);
90101
return _devExIssuesEventProcessorService.SaveAndProcessIssueCommentEventAsync(headers, issueCommentEvent!, action);
91102
}
103+
104+
protected override Task ProcessPullRequestWebhookAsync(WebhookHeaders headers, PullRequestEvent pullRequestEvent, PullRequestAction action)
105+
{
106+
// Only process issues events in the onboarding repository
107+
// Skip if the PR action is not supported
108+
if (_gitHubOptions?.RepoInventory != pullRequestEvent?.Repository?.Name
109+
|| !_allowedPullRequestActions.Contains(action))
110+
{
111+
_logger.LogInformation("--> Bypassing GitHub Webhook event for PR# {Number} with action: {Action} in repository {RepoName}", pullRequestEvent?.PullRequest?.Number, pullRequestEvent?.Action, pullRequestEvent?.Repository?.Name);
112+
return Task.CompletedTask;
113+
}
114+
115+
return _devExPullRequestEventProcessorService.SaveAndProcessPullRequestEventAsync(headers, pullRequestEvent!, action);
116+
}
92117
}
93118
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Octokit.Webhooks;
2+
using Octokit.Webhooks.Events;
3+
using Octokit.Webhooks.Events.PullRequest;
4+
5+
namespace DevExcelerateApi.Services
6+
{
7+
public interface IDevExPullRequestEventProcessorService
8+
{
9+
Task SaveAndProcessPullRequestEventAsync(WebhookHeaders headers, PullRequestEvent pullRequestEvent, PullRequestAction action);
10+
}
11+
}

0 commit comments

Comments
 (0)