diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..922a174 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: CI + +on: + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/DependencyUpdated.Core/Config/AzureDevOpsConfig.cs b/DependencyUpdated.Core/Config/AzureDevOpsConfig.cs index 940a980..923da54 100644 --- a/DependencyUpdated.Core/Config/AzureDevOpsConfig.cs +++ b/DependencyUpdated.Core/Config/AzureDevOpsConfig.cs @@ -15,6 +15,8 @@ public class AzureDevOpsConfig : IValidatableObject public string? Project { get; set; } public string? Repository { get; set; } + + public int? WorkItemId { get; set; } public string TargetBranchName { get; set; } = "dev"; diff --git a/DependencyUpdated.Repositories.AzureDevOps/AzureDevOps.cs b/DependencyUpdated.Repositories.AzureDevOps/AzureDevOps.cs index 72dbf81..59a0f76 100644 --- a/DependencyUpdated.Repositories.AzureDevOps/AzureDevOps.cs +++ b/DependencyUpdated.Repositories.AzureDevOps/AzureDevOps.cs @@ -93,48 +93,73 @@ public async Task SubmitPullRequest(IReadOnlyCollection updates, s $"https://dev.azure.com/{configValue.Organization}/{configValue.Project}/_apis/git/repositories/{configValue.Repository}/pullrequests?api-version=6.0"; logger.Information("Creating new PR"); - 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 pr = new PullRequest(sourceBranch, targetBranch, prTitile, prDescription); + 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 pr = new PullRequest(sourceBranch, targetBranch, prTitile, prDescription); - var jsonString = JsonSerializer.Serialize(pr); - var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); + var jsonString = JsonSerializer.Serialize(pr); + var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); - var result = await client.PostAsync(baseUrl, content); - result.EnsureSuccessStatusCode(); + var result = await client.PostAsync(baseUrl, content); + result.EnsureSuccessStatusCode(); - if (result.StatusCode == HttpStatusCode.NonAuthoritativeInformation) - { - throw new Exception("Invalid PAT token provided"); - } + if (result.StatusCode == HttpStatusCode.NonAuthoritativeInformation) + { + throw new Exception("Invalid PAT token provided"); + } - var response = await result.Content.ReadAsStringAsync(); - var options = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }; - var responseObject = JsonSerializer.Deserialize(response, options); - if (responseObject is null) - { - throw new Exception("Missing response from API"); - } + var response = await result.Content.ReadAsStringAsync(); + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }; + var responseObject = JsonSerializer.Deserialize(response, options); + if (responseObject is null) + { + throw new Exception("Missing response from API"); + } - logger.Information("New PR created {Id}", responseObject.PullRequestId); - if (configValue.AutoComplete) + 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(); + } + + if (configValue.WorkItemId.HasValue) + { + logger.Information("Setting work item {ConfigValueWorkItemId} relation to {PullRequestId}", + configValue.WorkItemId.Value, responseObject.PullRequestId); + var workItemUpdateUrl = new Uri( + $"https://dev.azure.com/{configValue.Organization}/{configValue.Project}/_apis/wit/workitems/{configValue.WorkItemId.Value}?api-version=6.0"); + var patchValue = new[] { - 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(); - } + new + { + op = "add", + path = "/relations/-", + value = new + { + rel = "ArtifactLink", + url = responseObject.ArtifactId, + attributes = new { name = "Pull Request" } + } + } + }; + + jsonString = JsonSerializer.Serialize(patchValue); + content = new StringContent(jsonString, Encoding.UTF8, "application/json-patch+json"); + result = await client.PatchAsync(workItemUpdateUrl, content); + result.EnsureSuccessStatusCode(); } } diff --git a/DependencyUpdated.Repositories.AzureDevOps/Dto/PullRequestResponse.cs b/DependencyUpdated.Repositories.AzureDevOps/Dto/PullRequestResponse.cs index fad6c58..f99eb51 100644 --- a/DependencyUpdated.Repositories.AzureDevOps/Dto/PullRequestResponse.cs +++ b/DependencyUpdated.Repositories.AzureDevOps/Dto/PullRequestResponse.cs @@ -1,3 +1,3 @@ namespace DependencyUpdated.Repositories.AzureDevOps.Dto; -public record PullRequestResponse(int PullRequestId, User CreatedBy); \ No newline at end of file +public record PullRequestResponse(int PullRequestId, User CreatedBy, string Url, string ArtifactId); \ No newline at end of file diff --git a/DependencyUpdated/config.json b/DependencyUpdated/config.json index 56a8855..05c6d6a 100644 --- a/DependencyUpdated/config.json +++ b/DependencyUpdated/config.json @@ -7,7 +7,8 @@ "PAT": "", "Organization": "", "Project": "", - "Repository": "" + "Repository": "", + "WorkItemId": null }, "PullRequestType": "AzureDevOps", "Projects": [