Skip to content

Commit

Permalink
Implement autoapprover + migrate to refit (#40)
Browse files Browse the repository at this point in the history
* Migrate to refit - work in progress.

* Approver

* Api fixes
  • Loading branch information
torbacz authored Oct 1, 2024
1 parent 1f412b3 commit 427b0a6
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 57 deletions.
4 changes: 4 additions & 0 deletions src/DependencyUpdated.Core/Config/AzureDevOpsConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public record AzureDevOpsConfig : IValidatableObject
public string? Email { get; set; }

public string? PAT { get; set; }

public string? ApproverPAT { get; set; }

public string? AutoApproverId { get; set; }

public string? Organization { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion src/DependencyUpdated.Core/Updater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ public async Task DoUpdate()
var projectFiles = updater.GetAllProjectFiles(directory);
var projectName = ResolveProjectName(project, directory);
var alreadyProcessed = new List<DependencyDetails>();
var allProjectDependencies = await updater.ExtractAllPackages(projectFiles);
foreach (var group in project.Groups)
{
repositoryProvider.SwitchToUpdateBranch(repositoryPath, projectName, group);
var allProjectDependencies = await updater.ExtractAllPackages(projectFiles);
var filteredPackages = FilterPackages(allProjectDependencies, alreadyProcessed, group, project);
if (filteredPackages.Count == 0)
{
Expand Down
21 changes: 15 additions & 6 deletions src/DependencyUpdated.Projects.DotNet/DotNetUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using NuGet.Configuration;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using System.Net;
using System.Xml;
using System.Xml.Linq;
using ILogger = Serilog.ILogger;
Expand Down Expand Up @@ -78,12 +79,20 @@ public async Task<IReadOnlyCollection<DependencyDetails>> GetVersions(Dependency
foreach (var repository in repositories)
{
var findPackageByIdResource = await repository.GetResourceAsync<FindPackageByIdResource>();
var versions = await findPackageByIdResource.GetAllVersionsAsync(
package.Name,
new SourceCacheContext(),
NullLogger.Instance,
CancellationToken.None);
allVersions.AddRange(versions.Where(x => !x.IsPrerelease));
try
{
var versions = await findPackageByIdResource.GetAllVersionsAsync(
package.Name,
new SourceCacheContext(),
NullLogger.Instance,
CancellationToken.None);
allVersions.AddRange(versions.Where(x => !x.IsPrerelease));
}
catch (FatalProtocolException ex)
when (ex.InnerException is HttpRequestException { StatusCode: HttpStatusCode.NotFound })
{
// Package not found in source
}
}

var result = allVersions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using DependencyUpdated.Core.Config;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Text;

namespace DependencyUpdated.Repositories.AzureDevOps;

internal sealed class AzureApiHeaderHandler(IOptions<UpdaterConfig> config) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var approverPat = config.Value.AzureDevOps.ApproverPAT;
if (request.RequestUri?.AbsolutePath.Contains(IAzureDevOpsClient.ReviewersResource) == true &&
!string.IsNullOrEmpty(approverPat))
{
request.Headers.Authorization ??= CreateToken(approverPat);
}

request.Headers.Authorization ??= CreateToken(config.Value.AzureDevOps.PAT!);
var response = await base.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
return response;
}

private static AuthenticationHeaderValue CreateToken(string token)
{
return new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($":{token}")));
}
}
74 changes: 25 additions & 49 deletions src/DependencyUpdated.Repositories.AzureDevOps/AzureDevOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
using Microsoft.Extensions.Options;
using Serilog;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace DependencyUpdated.Repositories.AzureDevOps;

[ExcludeFromCodeCoverage]
internal sealed class AzureDevOps(TimeProvider timeProvider, IOptions<UpdaterConfig> config, ILogger logger)
internal sealed class AzureDevOps(TimeProvider timeProvider, IOptions<UpdaterConfig> config, ILogger logger, IAzureDevOpsClient azureDevOpsClient)
: IRepositoryProvider
{
private const string GitCommitMessage = "Bump dependencies";
Expand Down Expand Up @@ -76,58 +75,32 @@ public void CommitChanges(string repositoryPath, string projectName, string grou

public async Task SubmitPullRequest(IReadOnlyCollection<UpdateResult> updates, string projectName, string group)
{
using var client = new HttpClient();
var configValue = config.Value.AzureDevOps;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($":{configValue.PAT}")));

var gitBranchName = CreateGitBranchName(projectName, config.Value.AzureDevOps.BranchName, group);
var sourceBranch = $"refs/heads/{gitBranchName}";
var targetBranch = $"refs/heads/{configValue.TargetBranchName}";
var baseUrl =
$"https://dev.azure.com/{configValue.Organization}/{configValue.Project}/_apis/git/repositories/{configValue.Repository}/pullrequests?api-version=6.0";

if (await CheckIfPrExists(client, baseUrl, sourceBranch, targetBranch))
if (await CheckIfPrExists(sourceBranch, targetBranch))
{
logger.Information("PR from {SourceBranch} to {TargetBranch} already exists. Skipping creating PR", gitBranchName, configValue.TargetBranchName);
logger.Information("PR from {SourceBranch} to {TargetBranch} already exists. Skipping creating PR",
gitBranchName, configValue.TargetBranchName);
return;
}

var prTitile = $"[AutoUpdate] Update dependencies - {projectName}";
var prDescription = CreatePrDescription(updates);

logger.Information("Creating new PR");
var pr = new PullRequest(sourceBranch, targetBranch, prTitile, prDescription);

var jsonString = JsonSerializer.Serialize(pr);
var content = new StringContent(jsonString, Encoding.UTF8, "application/json");

var result = await client.PostAsync(baseUrl, content);
result.EnsureSuccessStatusCode();

if (result.StatusCode == HttpStatusCode.NonAuthoritativeInformation)
{
throw new InvalidOperationException("Invalid PAT token provided");
}

var response = await result.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, };
var responseObject = JsonSerializer.Deserialize<PullRequestResponse>(response, options) ??
throw new InvalidOperationException("Missing response from API");

var responseObject = await azureDevOpsClient.CreatePullRequest(configValue.Repository!, pr);
logger.Information("New PR created {Id}", responseObject.PullRequestId);
if (configValue.AutoComplete)
{
logger.Information("Setting autocomplete for PR {Id}", responseObject.PullRequestId);
baseUrl =
$"https://dev.azure.com/{configValue.Organization}/{configValue.Project}/_apis/git/repositories/{configValue.Repository}/pullrequests/{responseObject.PullRequestId}?api-version=6.0";
var autoComplete = new PullRequestUpdate(responseObject.CreatedBy,
new GitPullRequestCompletionOptions(true, false, GitPullRequestMergeStrategy.Squash));
jsonString = JsonSerializer.Serialize(autoComplete);
content = new StringContent(jsonString, Encoding.UTF8, "application/json");
result = await client.PatchAsync(baseUrl, content);
result.EnsureSuccessStatusCode();
await azureDevOpsClient.UpdatePullRequest(configValue.Repository!, responseObject.PullRequestId,
autoComplete);
}

if (configValue.WorkItemId.HasValue)
Expand All @@ -151,11 +124,22 @@ public async Task SubmitPullRequest(IReadOnlyCollection<UpdateResult> updates, s
}
};

jsonString = JsonSerializer.Serialize(patchValue);
content = new StringContent(jsonString, Encoding.UTF8, "application/json-patch+json");
result = await client.PatchAsync(workItemUpdateUrl, content);
var jsonString = JsonSerializer.Serialize(patchValue);
var content = new StringContent(jsonString, Encoding.UTF8, "application/json-patch+json");
using var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($":{configValue.PAT}")));
var result = await client.PatchAsync(workItemUpdateUrl, content);
result.EnsureSuccessStatusCode();
}

if (!string.IsNullOrEmpty(configValue.AutoApproverId))
{
logger.Information("Setting auto approver to {ApproverId}", configValue.AutoApproverId);
await azureDevOpsClient.Approve(configValue.Repository!, responseObject.PullRequestId,
configValue.AutoApproverId, ApproveBody.Approve());
}
}

private static string CreateGitBranchName(string projectName, string branchName, string group)
Expand All @@ -165,18 +149,10 @@ private static string CreateGitBranchName(string projectName, string branchName,
return newBranchName;
}

private async Task<bool> CheckIfPrExists(HttpClient client, string baseUrl, string sourceBranchName,
string targetBranchName)
private async Task<bool> CheckIfPrExists(string sourceBranchName, string targetBranchName)
{
var response = await client.GetAsync(baseUrl);
response.EnsureSuccessStatusCode();

var jsonString = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, };
var responseObject = JsonSerializer.Deserialize<PullRequestArray>(jsonString, options) ??
throw new InvalidOperationException("Missing response from API");

return responseObject.Value.Any(pr => pr.SourceRefName == sourceBranchName && pr.TargetRefName == targetBranchName);
var response = await azureDevOpsClient.GetPullRequests(config.Value.AzureDevOps.Repository!);
return response.Value.Any(pr => pr.SourceRefName == sourceBranchName && pr.TargetRefName == targetBranchName);
}

private Branch? GetGitBranch(Repository repo, string branchName)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
using DependencyUpdated.Core.Config;
using DependencyUpdated.Core.Interfaces;
using DependencyUpdated.Core.Models.Enums;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Refit;
using System.Net.Http.Headers;
using System.Text.Json;

namespace DependencyUpdated.Repositories.AzureDevOps;

Expand All @@ -9,7 +14,21 @@ public static class ConfigureServices
public static IServiceCollection RegisterAzureDevOps(this IServiceCollection serviceCollection)
{
serviceCollection.AddKeyedSingleton<IRepositoryProvider, AzureDevOps>(RepositoryType.AzureDevOps);

serviceCollection.AddSingleton<AzureApiHeaderHandler>();
serviceCollection.AddRefitClient<IAzureDevOpsClient>(_ => new RefitSettings()
{
HttpMessageHandlerFactory = () => new LoggingHandler(),
ContentSerializer = new SystemTextJsonContentSerializer(new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true
})
}).ConfigureHttpClient((serviceProvider, x) =>
{
var options = serviceProvider.GetRequiredService<IOptions<UpdaterConfig>>().Value;
x.BaseAddress =
new Uri($"https://dev.azure.com/{options.AzureDevOps.Organization}/{options.AzureDevOps.Project}");
x.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}).AddHttpMessageHandler<AzureApiHeaderHandler>();
return serviceCollection;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<ItemGroup>
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Refit" Version="7.2.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="7.2.0" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace DependencyUpdated.Repositories.AzureDevOps.Dto;

public record ApproveBody(int Vote)
{
public static ApproveBody Approve()
{
return new ApproveBody(10);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace DependencyUpdated.Repositories.AzureDevOps.Dto;

public sealed record WorkItemUpdateAttributes(string Name);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace DependencyUpdated.Repositories.AzureDevOps.Dto;

public sealed record WorkItemUpdatePatch(string Op, string Path, WorkItemUpdateRelation Value);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace DependencyUpdated.Repositories.AzureDevOps.Dto;

public sealed record WorkItemUpdateRelation(string Rel, string Url, WorkItemUpdateAttributes Attributes);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using DependencyUpdated.Repositories.AzureDevOps.Dto;
using Refit;

namespace DependencyUpdated.Repositories.AzureDevOps;

public interface IAzureDevOpsClient
{
const string ReviewersResource = "reviewers";

[Get("/_apis/git/repositories/{repository}/pullrequests?api-version=6.0")]
Task<PullRequestArray> GetPullRequests(string repository);

[Post("/_apis/git/repositories/{repository}/pullrequests?api-version=6.0")]
Task<PullRequestResponse> CreatePullRequest(string repository, [Body] PullRequest pullRequestInfo);

[Patch("/_apis/git/repositories/{repository}/pullrequests/{pullRequestId}?api-version=6.0")]
Task<HttpResponseMessage> UpdatePullRequest(string repository, int pullRequestId, [Body] PullRequestUpdate pullRequestUpdate);

[Patch("/_apis/wit/workitems/{workItemId}?api-version=6.0")]
[Headers("Content-Type: application/json-patch+json")]
Task<HttpResponseMessage> UpdateWorkItemRelation(int workItemId, [Body] WorkItemUpdatePatch[] patchValue);

[Put("/_apis/git/repositories/{repository}/pullRequests/{pullRequestId}/" + ReviewersResource + "/{reviewerId}?api-version=6.0")]
Task<HttpResponseMessage> Approve(string repository, int pullRequestId, string reviewerId, [Body] ApproveBody body);
}
33 changes: 33 additions & 0 deletions src/DependencyUpdated.Repositories.AzureDevOps/LoggingHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Refit;
using System.Diagnostics;
using System.Net;

namespace DependencyUpdated.Repositories.AzureDevOps;

internal sealed class LoggingHandler : HttpClientHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
Debug.WriteLine("Request:");
Debug.WriteLine(request.ToString());
if (request.Content != null)
{
Debug.WriteLine(await request.Content.ReadAsStringAsync(cancellationToken));
}

Debug.WriteLine(string.Empty);

var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.NonAuthoritativeInformation)
{
throw await ApiException.Create("Invalid PAT token", request, request.Method, response, new RefitSettings());
}

Debug.WriteLine("Response:");
Debug.WriteLine(response.ToString());
Debug.WriteLine(await response.Content.ReadAsStringAsync(cancellationToken));

return response;
}
}

0 comments on commit 427b0a6

Please sign in to comment.