Skip to content

Commit add21f3

Browse files
author
Calin Lupas
authored
Merge pull request #24 from DevExcelerate/feature/20-validation-raise-pr
#20 #21 Add inventory support and refactor GitHub integration
2 parents 3b7b01f + f7b1ee6 commit add21f3

18 files changed

Lines changed: 466 additions & 155 deletions

src/DevExcelerateApi/Core/Options/GitHubOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ public class GitHubOptions
5252
[Required]
5353
public required bool IssueAutoApprove { get; set; }
5454

55+
/// <summary>
56+
/// Flag to save repository from issue event or use the inventory repository.
57+
/// </summary>
58+
[Required]
59+
public required bool UseInventoryWithPullRequest { get; set; }
60+
5561
/// <summary>
5662
/// The custom property name for the repository rulesets.
5763
/// </summary>

src/DevExcelerateApi/DevExcelerateApi.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
<PackageReference Include="Azure.Identity" Version="1.13.2" />
1212
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.7.0" />
1313
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.48.0" />
14-
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.7.0" />
14+
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.8.0" />
1515
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
1616
<PackageReference Include="Octokit" Version="14.0.0" />
1717
<PackageReference Include="Octokit.Webhooks.AspNetCore" Version="2.4.1" />
18-
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
18+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0" />
19+
<PackageReference Include="YamlDotNet" Version="16.3.0" />
1920
</ItemGroup>
2021

2122
<ItemGroup>
Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,109 @@
11
using Octokit;
2+
using Octokit.Helpers;
23

34
namespace DevExcelerateApi.Helpers
45
{
5-
public class GitHubHelper: IGitHubHelper
6+
public class GitHubHelper : IGitHubHelper
67
{
7-
public async Task CopyFilesAsync(IGitHubClient client, string sourceOwner, string sourceRepo, string sourcePath, string targetOwner, string targetRepo, string targetPath)
8+
private IGitHubClient? _githubClient;
9+
10+
public void SetGitHubClient(IGitHubClient githubClient)
811
{
9-
var sourceFiles = await client.Repository.Content.GetAllContents(sourceOwner, sourceRepo, sourcePath);
12+
_githubClient = githubClient ?? throw new ArgumentNullException(nameof(githubClient));
13+
}
14+
15+
public async Task CopyFilesAsync(string sourceOwner, string sourceRepo, string sourcePath, string targetOwner, string targetRepo, string targetPath)
16+
{
17+
if (_githubClient == null)
18+
{
19+
throw new InvalidOperationException("GitHub client is not configured.");
20+
}
21+
22+
var sourceFiles = await _githubClient.Repository.Content.GetAllContents(sourceOwner, sourceRepo, sourcePath);
1023

1124
foreach (var file in sourceFiles)
1225
{
1326
if (file.Type == ContentType.File)
1427
{
15-
var fileContent = await client.Repository.Content.GetRawContent(sourceOwner, sourceRepo, file.Path);
28+
var fileContent = await _githubClient.Repository.Content.GetRawContent(sourceOwner, sourceRepo, file.Path);
1629
var fileContentString = System.Text.Encoding.UTF8.GetString(fileContent);
1730
var createFileRequest = new CreateFileRequest($"Copying {file.Name} from {sourceRepo}", fileContentString);
1831

1932
var targetFilePath = $"{targetPath}/{file.Name}";
20-
await client.Repository.Content.CreateFile(targetOwner, targetRepo, targetFilePath, createFileRequest);
33+
await _githubClient.Repository.Content.CreateFile(targetOwner, targetRepo, targetFilePath, createFileRequest);
34+
}
35+
}
36+
}
37+
38+
public async Task<string> GetFileContentAsync(string owner, string repo, string path)
39+
{
40+
if (_githubClient == null)
41+
{
42+
throw new InvalidOperationException("GitHub client is not configured.");
43+
}
44+
45+
var fileContent = await _githubClient.Repository.Content.GetRawContent(owner, repo, path);
46+
var fileContentString = System.Text.Encoding.UTF8.GetString(fileContent);
47+
return fileContentString;
48+
}
49+
50+
public async Task<RepositoryContentChangeSet?> SaveFileAsync(string owner, string repo, string defaultBranchName, string branch, string path, string content)
51+
{
52+
if (_githubClient == null)
53+
{
54+
throw new InvalidOperationException("GitHub client is not configured.");
55+
}
56+
57+
try
58+
{
59+
// Check if the file already exists
60+
var existingFile = await _githubClient.Repository.Content.GetAllContentsByRef(owner, repo, path, branch);
61+
62+
if (existingFile != null && existingFile.Count > 0)
63+
{
64+
var updateFileRequest = new UpdateFileRequest($"Updating file {path}", content, existingFile[0].Sha, branch);
65+
return await _githubClient.Repository.Content.UpdateFile(owner, repo, path, updateFileRequest);
2166
}
2267
}
68+
catch (NotFoundException)
69+
{
70+
await ReferenceExtensions.CreateBranch(_githubClient.Git.Reference, owner, repo, branch, defaultBranchName);
71+
72+
var createFileRequest = new CreateFileRequest($"Adding file {path}", content, branch);
73+
74+
return await _githubClient.Repository.Content.CreateFile(owner, repo, path, createFileRequest);
75+
}
76+
77+
return null;
78+
}
79+
80+
public async Task<PullRequest> CreatePullRequestAsync(string owner, string repo, string headBranch, string baseBranch, string title, string body)
81+
{
82+
if (_githubClient == null)
83+
{
84+
throw new InvalidOperationException("GitHub client is not configured.");
85+
}
86+
87+
// Step 1: Check if a pull request already exists for the head branch
88+
var pullRequests = await _githubClient.PullRequest.GetAllForRepository(owner, repo, new PullRequestRequest
89+
{
90+
State = ItemStateFilter.Open, // Only check open pull requests
91+
Head = $"{owner}:{headBranch}" // Filter by the head branch
92+
});
93+
94+
if (pullRequests.Any())
95+
{
96+
// If a pull request already exists, return the first one
97+
return pullRequests[0];
98+
}
99+
100+
// Step 2: Create a new pull request if none exists
101+
var newPullRequest = new NewPullRequest(title, headBranch, baseBranch)
102+
{
103+
Body = body
104+
};
105+
106+
return await _githubClient.PullRequest.Create(owner, repo, newPullRequest);
23107
}
24108
}
25109
}

src/DevExcelerateApi/Helpers/IGitHubHelper.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ namespace DevExcelerateApi.Helpers
44
{
55
public interface IGitHubHelper
66
{
7-
Task CopyFilesAsync(IGitHubClient client, string sourceOwner, string sourceRepo, string sourcePath, string targetOwner, string targetRepo, string targetPath);
7+
void SetGitHubClient(IGitHubClient githubClient);
8+
Task CopyFilesAsync(string sourceOwner, string sourceRepo, string sourcePath, string targetOwner, string targetRepo, string targetPath);
9+
Task<string> GetFileContentAsync(string owner, string repo, string path);
10+
Task<RepositoryContentChangeSet?> SaveFileAsync(string owner, string repo, string defaultBranchName, string branch, string path, string content);
11+
Task<PullRequest> CreatePullRequestAsync(string owner, string repo, string headBranch, string baseBranch, string title, string body);
812
}
913
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using YamlDotNet.Serialization;
2+
using YamlDotNet.Serialization.NamingConventions;
3+
4+
namespace DevExcelerateApi.Helpers
5+
{
6+
public static class YamlHelpers
7+
{
8+
private static readonly ISerializer Serializer = new SerializerBuilder()
9+
.WithNamingConvention(PascalCaseNamingConvention.Instance)
10+
.WithNamingConvention(UnderscoredNamingConvention.Instance)
11+
.Build();
12+
13+
private static readonly IDeserializer Deserializer = new DeserializerBuilder()
14+
.WithNamingConvention(PascalCaseNamingConvention.Instance)
15+
.WithNamingConvention(UnderscoredNamingConvention.Instance)
16+
.IgnoreUnmatchedProperties()
17+
.IgnoreFields()
18+
.Build();
19+
20+
public static string Serialize<T>(T obj)
21+
{
22+
if (obj == null) throw new ArgumentNullException(nameof(obj));
23+
return Serializer.Serialize(obj);
24+
}
25+
26+
public static T Deserialize<T>(string yaml)
27+
{
28+
if (string.IsNullOrWhiteSpace(yaml)) throw new ArgumentException("YAML content cannot be null or empty.", nameof(yaml));
29+
return Deserializer.Deserialize<T>(yaml);
30+
}
31+
}
32+
}

src/DevExcelerateApi/Models/Constants.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ public static class Constants
44
{
55
// Core
66
public const char Comma = ',';
7-
7+
8+
// GitHub Policies
9+
public const string GitHubPoliciesFolderPath = ".github/policies/";
10+
public const string GitHubInventoryReposFolderPath = "repositories/";
11+
public const string GitHubDefaultBranch = "main";
12+
813
// Constants for the different types of messages that can be sent
914
public const string ERROR_REPO_CREATION = "ERROR_REPO_CREATION";
1015
public const string ERROR_REPO_EXISTS = "ERROR_REPO_EXISTS";
1116
public const string ERROR_REPO_TEAM_DOES_NOT_EXISTS = "ERROR_REPO_TEAM_DOES_NOT_EXISTS";
1217
public const string ERROR_REPO_RULESET_DOES_NOT_EXISTS = "ERROR_REPO_RULESET_DOES_NOT_EXISTS";
18+
public const string ERROR_REPO_PR_CREATION = "ERROR_REPO_PR_CREATION";
19+
public const string VALIDATION_PASSED = "VALIDATION_PASSED";
1320
public const string ISSUE_APPROVED = "ISSUE_APPROVED";
1421
public const string ISSUE_COMPLETED = "ISSUE_COMPLETED";
1522
public const string ISSUE_FAILED = "ISSUE_FAILED";
@@ -22,5 +29,6 @@ public static class Constants
2229
public const string TICKETSTATUS_OPENED = "TICKETSTATUS_OPENED";
2330
public const string TICKETSTATUS_REJECTED = "TICKETSTATUS_REJECTED";
2431
public const string TICKETSTATUS_VALIDATED = "TICKETSTATUS_VALIDATED";
32+
public const string PULL_REQUEST_BODY = "PULL_REQUEST_BODY";
2533
}
2634
}

src/DevExcelerateApi/Models/CreateRepositoryRequestModel.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
using DevExcelerateApi.Core.Extensions;
2+
using DevExcelerateApi.Helpers;
23

34
namespace DevExcelerateApi.Models
45
{
56
public class CreateRepositoryRequestModel : DevExIssueRequestModel
67
{
78
public string? RepositoryOwner { get; set; } // will be inherited from issue
89

9-
public string? RepositoryName { get; init; }
10+
public string? RepositoryName { get; set; }
1011

11-
public string? RepositoryVisibility { get; init; }
12+
public string? RepositoryVisibility { get; set; }
13+
14+
public string? RepositoryClassification { get; set; }
15+
16+
public string? RepositoryDescription { get; set; }
17+
18+
public string? RepositoryDefaultBranch { get; set; }
1219

13-
public string? RepositoryDescription { get; init; }
14-
1520
public string? RepositoryMaintainers { get; set; } // will be inherited from issue
1621

17-
public string? RepositoryReaders { get; init; }
22+
public string? RepositoryReaders { get; set; }
1823

19-
public string? RepositoryContributors { get; init; }
24+
public string? RepositoryContributors { get; set; }
2025

21-
public string? RepositoryRulesets { get; init; }
26+
public string? RepositoryRulesets { get; set; }
2227

2328
public static new CreateRepositoryRequestModel Parse(IDictionary<string, object?> values)
2429
{
@@ -27,6 +32,7 @@ public class CreateRepositoryRequestModel : DevExIssueRequestModel
2732
values.TryGetValue("repository_owner", out var repositoryOwner);
2833
values.TryGetValue("repository_name", out var repositoryName);
2934
values.TryGetValue("repository_visibility", out var repositoryVisibility);
35+
values.TryGetValue("repository_classification", out var repositoryClassification);
3036
values.TryGetValue("repository_description", out var repositoryDescription);
3137
values.TryGetValue("repository_maintainers", out var repositoryMaintainers);
3238
values.TryGetValue("repository_readers", out var repositoryReaders);
@@ -39,6 +45,7 @@ public class CreateRepositoryRequestModel : DevExIssueRequestModel
3945
RepositoryOwner = repositoryOwner?.ToString(),
4046
RepositoryName = repositoryName?.ToString()?.SanitizeRepositoryName(),
4147
RepositoryVisibility = repositoryVisibility?.ToString(),
48+
RepositoryClassification = repositoryClassification?.ToString(),
4249
RepositoryDescription = repositoryDescription?.ToString(),
4350
RepositoryMaintainers = repositoryMaintainers?.ToString(),
4451
RepositoryReaders = repositoryReaders?.ToString(),
@@ -51,8 +58,21 @@ public bool IsValid()
5158
{
5259
return !string.IsNullOrEmpty(RepositoryName) &&
5360
!string.IsNullOrEmpty(RepositoryVisibility) &&
61+
!string.IsNullOrEmpty(RepositoryClassification) &&
5462
!string.IsNullOrEmpty(RepositoryDescription) &&
55-
!string.IsNullOrEmpty(RepositoryContributors);
63+
!string.IsNullOrEmpty(RepositoryContributors) &&
64+
!string.IsNullOrEmpty(RepositoryRulesets);
65+
}
66+
67+
public string SerializeToYaml()
68+
{
69+
return YamlHelpers.Serialize(this);
70+
}
71+
72+
public static CreateRepositoryRequestModel DeserializeFromYaml(string yamlContent)
73+
{
74+
ArgumentNullException.ThrowIfNull(yamlContent);
75+
return YamlHelpers.Deserialize<CreateRepositoryRequestModel>(yamlContent);
5676
}
5777
}
5878
}

src/DevExcelerateApi/Models/DevExIssueRequestModel.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@
22
{
33
public class DevExIssueRequestModel
44
{
5+
public string? Name { get; set; }
6+
7+
public string? Description { get; set; }
8+
9+
public string? Resource { get; set; }
10+
511
public RequestType RequestType { get; set; }
612

13+
public long? RequestIssue { get; set; }
14+
15+
public string? RequestIssueHtmlUrl { get; set; }
16+
17+
public string? RequestUser { get; set; }
18+
719
public static DevExIssueRequestModel Parse(IDictionary<string, object?> values)
820
{
921
ArgumentNullException.ThrowIfNull(values);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.ComponentModel;
2+
3+
namespace DevExcelerateApi.Models
4+
{
5+
public enum DevExPolicies
6+
{
7+
[Description("Repository Metadata")]
8+
Repository_Metadata = 1,
9+
[Description("Repository Protection")]
10+
Repository_Protection
11+
}
12+
13+
public static class DevExPoliciesExtensions
14+
{
15+
public static string GetPolicyYamlFileName(this DevExPolicies policy)
16+
{
17+
return policy switch
18+
{
19+
DevExPolicies.Repository_Metadata => "repository-metadata.yml",
20+
DevExPolicies.Repository_Protection => "repository-protection.yml",
21+
_ => throw new ArgumentOutOfRangeException(nameof(policy), policy, null)
22+
};
23+
}
24+
}
25+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@
123123
<data name="ERROR_REPO_EXISTS" xml:space="preserve">
124124
<value>Le nom du dépôt existe déjà au sein de l'organisation.</value>
125125
</data>
126+
<data name="ERROR_REPO_PR_CREATION" xml:space="preserve">
127+
<value>Un problème est survenu lors de la tentative de création de la demande de tirage.</value>
128+
</data>
126129
<data name="ERROR_REPO_RULESET_DOES_NOT_EXISTS" xml:space="preserve">
127130
<value>Le set de règles '{0}' n'existe pas.</value>
128131
</data>
@@ -138,4 +141,10 @@
138141
<data name="ISSUE_VALIDATED" xml:space="preserve">
139142
<value>Votre demande a été validée et le dépôt sera créé sous peu.</value>
140143
</data>
144+
<data name="PULL_REQUEST_BODY" xml:space="preserve">
145+
<value>Demande de création d'une nouvelle ressource avec la configuration suivante en tant que code :</value>
146+
</data>
147+
<data name="VALIDATION_PASSED" xml:space="preserve">
148+
<value>Toutes les vérifications de validation ont été effectuées avec succès.</value>
149+
</data>
141150
</root>

0 commit comments

Comments
 (0)