Created
October 22, 2024 20:06
-
-
Save jennings/4cb0b228e6eacacad4e07ebc42d18de9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// <PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.239.0-preview" /> | |
// <PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="19.239.0-preview" /> | |
public class RetargetPullRequests | |
{ | |
private readonly ILogger _log; | |
private readonly AzureDevOpsConfiguration _config; | |
private readonly GitHttpClient _gitClient; | |
public RetargetPullRequests(ILogger<RetargetPullRequests> log, IOptions<AzureDevOpsConfiguration> config, VssConnection connection) | |
{ | |
_log = log; | |
_config = config.Value; | |
_gitClient = connection.GetClient<GitHttpClient>(); | |
} | |
[Function("RetargetPullRequests")] | |
public async Task Run([QueueTrigger("retarget-pull-requests")] QueueMessage message, FunctionContext context) | |
{ | |
var cloudEvent = CloudEvent.Parse(message.Body); | |
if (cloudEvent == null) | |
{ | |
_log.LogError("Received message body that was JSON but not a valid CloudEvent"); | |
throw new InvalidOperationException("Cannot process message body"); | |
} | |
_log.LogInformation("Received {EventType} event with ID {Id}", cloudEvent.Type, cloudEvent.Id); | |
if (cloudEvent.Type != "com.azure.dev.git.pullrequest.merged") | |
{ | |
_log.LogInformation("Not interested in event"); | |
return; | |
} | |
var webhook = cloudEvent.Data!.ToObjectFromJson<PullRequestMergedWebhook>(); | |
await ExecuteAsync(webhook, default); | |
} | |
public async Task ExecuteAsync(PullRequestMergedWebhook webhook, CancellationToken cancellationToken) | |
{ | |
if (!await ShouldTriggerRetargetingAsync(webhook, cancellationToken)) | |
{ | |
return; | |
} | |
await RetargetPullRequestsAsync(webhook, cancellationToken); | |
await DeleteSourceRefAsync(webhook, cancellationToken); | |
} | |
private async Task RetargetPullRequestsAsync(PullRequestMergedWebhook webhook, CancellationToken cancellationToken) | |
{ | |
_log.LogInformation("Fetching pull requests targeting {TargetRefName}", webhook.Resource.SourceRefName); | |
var pullRequests = await _gitClient.GetPullRequestsByProjectAsync(webhook.Resource.Repository.Project.Id, new() | |
{ | |
RepositoryId = webhook.Resource.Repository.Id, | |
Status = PullRequestStatus.Active, | |
TargetRefName = webhook.Resource.SourceRefName, | |
}); | |
foreach (var pullRequest in pullRequests) | |
{ | |
var update = new GitPullRequest() | |
{ | |
TargetRefName = webhook.Resource.TargetRefName, | |
}; | |
_log.LogInformation("Retargeting pull request {PullRequestId} from {CurrentTargetRefName} to {TargetRefName}", pullRequest.PullRequestId, pullRequest.TargetRefName, update.TargetRefName); | |
await _gitClient.UpdatePullRequestAsync(update, webhook.Resource.Repository.Id, pullRequest.PullRequestId, cancellationToken); | |
} | |
} | |
private async Task DeleteSourceRefAsync(PullRequestMergedWebhook webhook, CancellationToken cancellationToken) | |
{ | |
const string DELETE_REF = "0000000000000000000000000000000000000000"; | |
var update = new GitRefUpdate | |
{ | |
Name = webhook.Resource.SourceRefName, | |
// Only delete the ref if it's still in the same position it was when the PR was merged | |
OldObjectId = webhook.Resource.LastMergeSourceCommit.CommitId, | |
NewObjectId = DELETE_REF, | |
}; | |
try | |
{ | |
var changes = await _gitClient.UpdateRefsAsync(new List<GitRefUpdate> { update }, webhook.Resource.Repository.Id, cancellationToken: cancellationToken); | |
foreach (var result in changes) | |
{ | |
_log.Log( | |
result.Success ? LogLevel.Information : LogLevel.Warning, | |
"Update to ref {SourceRefName} Status={Status}: {Message}", | |
result.Name, | |
result.UpdateStatus, | |
result.CustomMessage | |
); | |
} | |
} | |
catch (Exception ex) | |
{ | |
_log.LogError(ex, "Unable to delete source branch"); | |
} | |
} | |
private async Task<bool> ShouldTriggerRetargetingAsync(PullRequestMergedWebhook webhook, CancellationToken cancellationToken) | |
{ | |
// pull request is not merged yet | |
if (webhook.Resource.Status != "completed") | |
{ | |
_log.LogInformation("Pull request {PullRequestId} is not completed, has status {Status}", webhook.Resource.PullRequestId, webhook.Resource.Status); | |
return false; | |
} | |
// source ref is a branch | |
if (!TryGetBranchName(webhook.Resource.SourceRefName, out var sourceBranchName)) | |
{ | |
_log.LogInformation("Merged ref {SourceRefName} is not a branch", webhook.Resource.SourceRefName); | |
return false; | |
} | |
// configured to delete source branch | |
var pullRequest = await _gitClient.GetPullRequestAsync(webhook.Resource.Repository.Id, webhook.Resource.PullRequestId, cancellationToken: cancellationToken); | |
if (!pullRequest.CompletionOptions.DeleteSourceBranch) | |
{ | |
_log.LogInformation("Pull request {PullRequestId} option to delete source branch was not set", webhook.Resource.SourceRefName); | |
return false; | |
} | |
// source branch exists | |
try | |
{ | |
var branch = await _gitClient.GetBranchAsync(webhook.Resource.Repository.Id, sourceBranchName, cancellationToken: cancellationToken); | |
_log.LogInformation("Branch {SourceBranchName} still exists", sourceBranchName); | |
return true; | |
} | |
catch (VssServiceResponseException ex) | |
{ | |
_log.LogWarning(ex, "Exception while getting branch"); | |
_log.LogInformation("Branch {SourceBranchName} no longer exists", sourceBranchName); | |
return false; | |
} | |
} | |
bool TryGetBranchName(string refName, [NotNullWhen(returnValue: true)] out string? branchName) | |
{ | |
const string BRANCH_PREFIX = "refs/heads/"; | |
if (refName.StartsWith(BRANCH_PREFIX)) | |
{ | |
branchName = refName.Substring(BRANCH_PREFIX.Length); | |
return true; | |
} | |
branchName = null; | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment