Skip to content

Commit 6a4b4e6

Browse files
Merge pull request #1416 from georgy-gorelko/ado-pipeline-id
Enhance RewirePipeline command to support optional pipeline ID vs pipeline name
2 parents c4aef59 + ce9c4c0 commit 6a4b4e6

File tree

8 files changed

+450
-31
lines changed

8 files changed

+450
-31
lines changed

src/Octoshift/Services/AdoPipelineTriggerService.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,33 @@ public virtual async Task RewirePipelineToGitHub(
5151
{
5252
var url = $"{_adoBaseUrl}/{adoOrg.EscapeDataString()}/{teamProject.EscapeDataString()}/_apis/build/definitions/{pipelineId}?api-version=6.0";
5353

54-
var response = await _adoApi.GetAsync(url);
55-
var data = JObject.Parse(response);
54+
try
55+
{
56+
var response = await _adoApi.GetAsync(url);
57+
var data = JObject.Parse(response);
5658

57-
var newRepo = CreateGitHubRepositoryConfiguration(githubOrg, githubRepo, defaultBranch, clean, checkoutSubmodules, connectedServiceId, targetApiUrl);
58-
var currentRepoName = data["repository"]?["name"]?.ToString();
59-
var isPipelineRequiredByBranchPolicy = await IsPipelineRequiredByBranchPolicy(adoOrg, teamProject, currentRepoName, pipelineId);
59+
var newRepo = CreateGitHubRepositoryConfiguration(githubOrg, githubRepo, defaultBranch, clean, checkoutSubmodules, connectedServiceId, targetApiUrl);
60+
var currentRepoName = data["repository"]?["name"]?.ToString();
61+
var isPipelineRequiredByBranchPolicy = await IsPipelineRequiredByBranchPolicy(adoOrg, teamProject, currentRepoName, pipelineId);
6062

61-
LogBranchPolicyCheckResults(pipelineId, isPipelineRequiredByBranchPolicy);
63+
LogBranchPolicyCheckResults(pipelineId, isPipelineRequiredByBranchPolicy);
6264

63-
var payload = BuildPipelinePayload(data, newRepo, originalTriggers, isPipelineRequiredByBranchPolicy);
65+
var payload = BuildPipelinePayload(data, newRepo, originalTriggers, isPipelineRequiredByBranchPolicy);
6466

65-
await _adoApi.PutAsync(url, payload.ToObject(typeof(object)));
67+
await _adoApi.PutAsync(url, payload.ToObject(typeof(object)));
68+
}
69+
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
70+
{
71+
// Pipeline not found - log warning and skip
72+
_log.LogWarning($"Pipeline {pipelineId} not found in {adoOrg}/{teamProject}. Skipping pipeline rewiring.");
73+
return;
74+
}
75+
catch (HttpRequestException ex)
76+
{
77+
// Other HTTP errors during pipeline retrieval
78+
_log.LogWarning($"HTTP error retrieving pipeline {pipelineId} in {adoOrg}/{teamProject}: {ex.Message}. Skipping pipeline rewiring.");
79+
return;
80+
}
6681
}
6782

6883
/// <summary>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using Moq;
6+
using Newtonsoft.Json.Linq;
7+
using OctoshiftCLI.Extensions;
8+
using OctoshiftCLI.Services;
9+
using Xunit;
10+
11+
namespace OctoshiftCLI.Tests.Octoshift.Services
12+
{
13+
public class AdoPipelineTriggerService_ErrorHandlingTests
14+
{
15+
private const string ADO_ORG = "foo-org";
16+
private const string TEAM_PROJECT = "foo-project";
17+
private const string REPO_NAME = "foo-repo";
18+
private const string PIPELINE_NAME = "CI Pipeline";
19+
private const int PIPELINE_ID = 123;
20+
private const string ADO_SERVICE_URL = "https://dev.azure.com";
21+
22+
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
23+
private readonly Mock<AdoApi> _mockAdoApi = TestHelpers.CreateMock<AdoApi>();
24+
private readonly AdoPipelineTriggerService _triggerService;
25+
26+
public AdoPipelineTriggerService_ErrorHandlingTests()
27+
{
28+
_triggerService = new AdoPipelineTriggerService(_mockAdoApi.Object, _mockOctoLogger.Object, ADO_SERVICE_URL);
29+
}
30+
31+
[Fact]
32+
public async Task RewirePipelineToGitHub_Should_Skip_When_Pipeline_Not_Found_404()
33+
{
34+
// Arrange
35+
var githubOrg = "github-org";
36+
var githubRepo = "github-repo";
37+
var serviceConnectionId = Guid.NewGuid().ToString();
38+
var defaultBranch = "main";
39+
var clean = "true";
40+
var checkoutSubmodules = "false";
41+
42+
var pipelineUrl = $"{ADO_SERVICE_URL}/{ADO_ORG.EscapeDataString()}/{TEAM_PROJECT.EscapeDataString()}/_apis/build/definitions/{PIPELINE_ID}?api-version=6.0";
43+
44+
// Mock 404 error when trying to get pipeline definition
45+
_mockAdoApi.Setup(x => x.GetAsync(pipelineUrl))
46+
.ThrowsAsync(new HttpRequestException("Response status code does not indicate success: 404 (Not Found)."));
47+
48+
// Act & Assert - Should not throw exception, should handle gracefully
49+
await _triggerService.Invoking(x => x.RewirePipelineToGitHub(
50+
ADO_ORG, TEAM_PROJECT, PIPELINE_ID, defaultBranch, clean, checkoutSubmodules,
51+
githubOrg, githubRepo, serviceConnectionId, null, null))
52+
.Should().NotThrowAsync();
53+
54+
// Verify that warning was logged
55+
_mockOctoLogger.Verify(x => x.LogWarning(It.Is<string>(s =>
56+
s.Contains("Pipeline 123 not found") &&
57+
s.Contains("Skipping pipeline rewiring"))), Times.Once);
58+
59+
// Verify that PutAsync was never called since we should skip the operation
60+
_mockAdoApi.Verify(x => x.PutAsync(It.IsAny<string>(), It.IsAny<object>()), Times.Never);
61+
}
62+
63+
[Fact]
64+
public async Task RewirePipelineToGitHub_Should_Skip_When_Pipeline_HTTP_Error()
65+
{
66+
// Arrange
67+
var githubOrg = "github-org";
68+
var githubRepo = "github-repo";
69+
var serviceConnectionId = Guid.NewGuid().ToString();
70+
var defaultBranch = "main";
71+
var clean = "true";
72+
var checkoutSubmodules = "false";
73+
74+
var pipelineUrl = $"{ADO_SERVICE_URL}/{ADO_ORG.EscapeDataString()}/{TEAM_PROJECT.EscapeDataString()}/_apis/build/definitions/{PIPELINE_ID}?api-version=6.0";
75+
76+
// Mock HTTP error (not 404) when trying to get pipeline definition
77+
_mockAdoApi.Setup(x => x.GetAsync(pipelineUrl))
78+
.ThrowsAsync(new HttpRequestException("Response status code does not indicate success: 500 (Internal Server Error)."));
79+
80+
// Act & Assert - Should not throw exception, should handle gracefully
81+
await _triggerService.Invoking(x => x.RewirePipelineToGitHub(
82+
ADO_ORG, TEAM_PROJECT, PIPELINE_ID, defaultBranch, clean, checkoutSubmodules,
83+
githubOrg, githubRepo, serviceConnectionId, null, null))
84+
.Should().NotThrowAsync();
85+
86+
// Verify that warning was logged
87+
_mockOctoLogger.Verify(x => x.LogWarning(It.Is<string>(s =>
88+
s.Contains("HTTP error retrieving pipeline 123") &&
89+
s.Contains("Skipping pipeline rewiring"))), Times.Once);
90+
91+
// Verify that PutAsync was never called since we should skip the operation
92+
_mockAdoApi.Verify(x => x.PutAsync(It.IsAny<string>(), It.IsAny<object>()), Times.Never);
93+
}
94+
95+
[Fact]
96+
public async Task RewirePipelineToGitHub_Should_Continue_When_Pipeline_Found()
97+
{
98+
// Arrange
99+
var githubOrg = "github-org";
100+
var githubRepo = "github-repo";
101+
var serviceConnectionId = Guid.NewGuid().ToString();
102+
var defaultBranch = "main";
103+
var clean = "true";
104+
var checkoutSubmodules = "false";
105+
106+
var existingPipelineData = new
107+
{
108+
name = PIPELINE_NAME,
109+
repository = new { name = REPO_NAME },
110+
triggers = new JArray()
111+
};
112+
113+
var pipelineUrl = $"{ADO_SERVICE_URL}/{ADO_ORG.EscapeDataString()}/{TEAM_PROJECT.EscapeDataString()}/_apis/build/definitions/{PIPELINE_ID}?api-version=6.0";
114+
var repoUrl = $"{ADO_SERVICE_URL}/{ADO_ORG.EscapeDataString()}/{TEAM_PROJECT.EscapeDataString()}/_apis/git/repositories/{REPO_NAME.EscapeDataString()}?api-version=6.0";
115+
116+
// Mock successful pipeline retrieval
117+
_mockAdoApi.Setup(x => x.GetAsync(pipelineUrl))
118+
.ReturnsAsync(existingPipelineData.ToJson());
119+
120+
// Mock repository lookup for branch policy check
121+
var repositoryId = "repo-123";
122+
var repoResponse = new { id = repositoryId, name = REPO_NAME }.ToJson();
123+
_mockAdoApi.Setup(x => x.GetAsync(repoUrl))
124+
.ReturnsAsync(repoResponse);
125+
126+
// Mock branch policies (empty)
127+
var policies = new { count = 0, value = Array.Empty<object>() }.ToJson();
128+
var policyUrl = $"{ADO_SERVICE_URL}/{ADO_ORG.EscapeDataString()}/{TEAM_PROJECT.EscapeDataString()}/_apis/policy/configurations?repositoryId={repositoryId}&api-version=6.0";
129+
_mockAdoApi.Setup(x => x.GetAsync(policyUrl))
130+
.ReturnsAsync(policies);
131+
132+
// Act
133+
await _triggerService.RewirePipelineToGitHub(
134+
ADO_ORG, TEAM_PROJECT, PIPELINE_ID, defaultBranch, clean, checkoutSubmodules,
135+
githubOrg, githubRepo, serviceConnectionId, null, null);
136+
137+
// Assert - Verify that PutAsync was called (pipeline was successfully rewired)
138+
_mockAdoApi.Verify(x => x.PutAsync(pipelineUrl, It.IsAny<object>()), Times.Once);
139+
140+
// Verify that no error warnings were logged
141+
_mockOctoLogger.Verify(x => x.LogWarning(It.Is<string>(s =>
142+
s.Contains("not found") || s.Contains("HTTP error"))), Times.Never);
143+
}
144+
}
145+
}

src/OctoshiftCLI.Tests/ado2gh/Commands/RewirePipeline/RewirePipelineCommandHandlerTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,64 @@ public async Task Uses_TargetApiUrl_When_Provided()
105105

106106
_mockAdoPipelineTriggerService.Verify(x => x.RewirePipelineToGitHub(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, GITHUB_REPO, SERVICE_CONNECTION_ID, triggers, targetApiUrl));
107107
}
108+
109+
[Fact]
110+
public async Task Validates_Neither_Pipeline_Name_Nor_Id_Provided()
111+
{
112+
var args = new RewirePipelineCommandArgs
113+
{
114+
AdoOrg = ADO_ORG,
115+
AdoTeamProject = ADO_TEAM_PROJECT,
116+
GithubOrg = GITHUB_ORG,
117+
GithubRepo = GITHUB_REPO,
118+
ServiceConnectionId = SERVICE_CONNECTION_ID,
119+
};
120+
121+
await Assert.ThrowsAsync<OctoshiftCliException>(() => _handler.Handle(args));
122+
}
123+
124+
[Fact]
125+
public async Task Validates_Both_Pipeline_Name_And_Id_Provided()
126+
{
127+
var args = new RewirePipelineCommandArgs
128+
{
129+
AdoOrg = ADO_ORG,
130+
AdoTeamProject = ADO_TEAM_PROJECT,
131+
AdoPipeline = ADO_PIPELINE,
132+
AdoPipelineId = 123,
133+
GithubOrg = GITHUB_ORG,
134+
GithubRepo = GITHUB_REPO,
135+
ServiceConnectionId = SERVICE_CONNECTION_ID,
136+
};
137+
138+
await Assert.ThrowsAsync<OctoshiftCliException>(() => _handler.Handle(args));
139+
}
140+
141+
[Fact]
142+
public async Task Uses_Pipeline_Id_When_Provided()
143+
{
144+
var pipelineId = 1234;
145+
var defaultBranch = "default-branch";
146+
var clean = "true";
147+
var checkoutSubmodules = "null";
148+
var triggers = new JArray(); // Mock triggers data
149+
150+
_mockAdoApi.Setup(x => x.GetPipeline(ADO_ORG, ADO_TEAM_PROJECT, pipelineId).Result).Returns((defaultBranch, clean, checkoutSubmodules, triggers));
151+
152+
var args = new RewirePipelineCommandArgs
153+
{
154+
AdoOrg = ADO_ORG,
155+
AdoTeamProject = ADO_TEAM_PROJECT,
156+
AdoPipelineId = pipelineId,
157+
GithubOrg = GITHUB_ORG,
158+
GithubRepo = GITHUB_REPO,
159+
ServiceConnectionId = SERVICE_CONNECTION_ID,
160+
};
161+
162+
await _handler.Handle(args);
163+
164+
// Verify that GetPipelineId is NOT called when ID is provided directly
165+
_mockAdoApi.Verify(x => x.GetPipelineId(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
166+
_mockAdoPipelineTriggerService.Verify(x => x.RewirePipelineToGitHub(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, GITHUB_REPO, SERVICE_CONNECTION_ID, triggers, null));
167+
}
108168
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using Moq;
6+
using Newtonsoft.Json.Linq;
7+
using OctoshiftCLI.AdoToGithub.Commands.RewirePipeline;
8+
using OctoshiftCLI.Services;
9+
using Xunit;
10+
11+
namespace OctoshiftCLI.Tests.AdoToGithub.Commands.RewirePipeline
12+
{
13+
public class RewirePipelineCommandHandler_ErrorHandlingTests
14+
{
15+
private const string ADO_ORG = "FooOrg";
16+
private const string ADO_TEAM_PROJECT = "BlahTeamProject";
17+
private const string ADO_PIPELINE = "FooPipeline";
18+
private const int PIPELINE_ID = 123;
19+
private const string GITHUB_ORG = "GitHubOrg";
20+
private const string GITHUB_REPO = "GitHubRepo";
21+
private const string SERVICE_CONNECTION_ID = "1234";
22+
23+
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
24+
private readonly Mock<AdoApi> _mockAdoApi = TestHelpers.CreateMock<AdoApi>();
25+
private readonly Mock<AdoPipelineTriggerService> _mockAdoPipelineTriggerService;
26+
private readonly RewirePipelineCommandHandler _handler;
27+
28+
public RewirePipelineCommandHandler_ErrorHandlingTests()
29+
{
30+
_mockAdoPipelineTriggerService = new Mock<AdoPipelineTriggerService>(_mockAdoApi.Object, _mockOctoLogger.Object, "https://dev.azure.com");
31+
_handler = new RewirePipelineCommandHandler(_mockOctoLogger.Object, _mockAdoApi.Object, _mockAdoPipelineTriggerService.Object);
32+
}
33+
34+
[Fact]
35+
public async Task HandleRegularRewire_Should_Throw_OctoshiftCliException_When_Pipeline_Not_Found()
36+
{
37+
// Arrange
38+
_mockAdoApi.Setup(x => x.GetPipelineId(ADO_ORG, ADO_TEAM_PROJECT, ADO_PIPELINE))
39+
.ReturnsAsync(PIPELINE_ID);
40+
41+
_mockAdoApi.Setup(x => x.GetPipeline(ADO_ORG, ADO_TEAM_PROJECT, PIPELINE_ID))
42+
.ThrowsAsync(new HttpRequestException("Response status code does not indicate success: 404 (Not Found)."));
43+
44+
var args = new RewirePipelineCommandArgs
45+
{
46+
AdoOrg = ADO_ORG,
47+
AdoTeamProject = ADO_TEAM_PROJECT,
48+
AdoPipeline = ADO_PIPELINE,
49+
GithubOrg = GITHUB_ORG,
50+
GithubRepo = GITHUB_REPO,
51+
ServiceConnectionId = SERVICE_CONNECTION_ID,
52+
DryRun = false
53+
};
54+
55+
// Act & Assert
56+
await _handler.Invoking(x => x.Handle(args))
57+
.Should().ThrowExactlyAsync<OctoshiftCliException>()
58+
.WithMessage("Pipeline could not be found. Please verify the pipeline name or ID and try again.");
59+
60+
// Verify error was logged
61+
_mockOctoLogger.Verify(x => x.LogError(It.Is<string>(s => s.Contains("Pipeline not found"))), Times.Once);
62+
}
63+
64+
[Fact]
65+
public async Task HandleRegularRewire_Should_Throw_OctoshiftCliException_When_Pipeline_Lookup_Fails()
66+
{
67+
// Arrange
68+
_mockAdoApi.Setup(x => x.GetPipelineId(ADO_ORG, ADO_TEAM_PROJECT, ADO_PIPELINE))
69+
.ThrowsAsync(new ArgumentException("Unable to find the specified pipeline", "pipeline"));
70+
71+
var args = new RewirePipelineCommandArgs
72+
{
73+
AdoOrg = ADO_ORG,
74+
AdoTeamProject = ADO_TEAM_PROJECT,
75+
AdoPipeline = ADO_PIPELINE,
76+
GithubOrg = GITHUB_ORG,
77+
GithubRepo = GITHUB_REPO,
78+
ServiceConnectionId = SERVICE_CONNECTION_ID,
79+
DryRun = false
80+
};
81+
82+
// Act & Assert
83+
await _handler.Invoking(x => x.Handle(args))
84+
.Should().ThrowExactlyAsync<OctoshiftCliException>()
85+
.WithMessage("Unable to find the specified pipeline. Please verify the pipeline name and try again.");
86+
87+
// Verify error was logged
88+
_mockOctoLogger.Verify(x => x.LogError(It.Is<string>(s => s.Contains("Pipeline lookup failed"))), Times.Once);
89+
}
90+
91+
[Fact]
92+
public async Task HandleRegularRewire_Should_Succeed_When_Pipeline_Found()
93+
{
94+
// Arrange
95+
var defaultBranch = "main";
96+
var clean = "true";
97+
var checkoutSubmodules = "false";
98+
var triggers = new JArray();
99+
100+
_mockAdoApi.Setup(x => x.GetPipelineId(ADO_ORG, ADO_TEAM_PROJECT, ADO_PIPELINE))
101+
.ReturnsAsync(PIPELINE_ID);
102+
103+
_mockAdoApi.Setup(x => x.GetPipeline(ADO_ORG, ADO_TEAM_PROJECT, PIPELINE_ID))
104+
.ReturnsAsync((defaultBranch, clean, checkoutSubmodules, triggers));
105+
106+
var args = new RewirePipelineCommandArgs
107+
{
108+
AdoOrg = ADO_ORG,
109+
AdoTeamProject = ADO_TEAM_PROJECT,
110+
AdoPipeline = ADO_PIPELINE,
111+
GithubOrg = GITHUB_ORG,
112+
GithubRepo = GITHUB_REPO,
113+
ServiceConnectionId = SERVICE_CONNECTION_ID,
114+
DryRun = false
115+
};
116+
117+
// Act
118+
await _handler.Handle(args);
119+
120+
// Assert
121+
_mockAdoPipelineTriggerService.Verify(x => x.RewirePipelineToGitHub(
122+
ADO_ORG, ADO_TEAM_PROJECT, PIPELINE_ID, defaultBranch, clean, checkoutSubmodules,
123+
GITHUB_ORG, GITHUB_REPO, SERVICE_CONNECTION_ID, triggers, null), Times.Once);
124+
125+
// Verify success was logged
126+
_mockOctoLogger.Verify(x => x.LogSuccess("Successfully rewired pipeline"), Times.Once);
127+
128+
// Verify no errors were logged
129+
_mockOctoLogger.Verify(x => x.LogError(It.IsAny<string>()), Times.Never);
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)