Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@

- **ado2gh rewire-pipeline**: Enhanced Azure DevOps pipeline trigger preservation to properly enable both CI and PR triggers with YAML control settings, ensuring triggers appear as enabled in ADO UI while deferring to YAML definitions
507 changes: 503 additions & 4 deletions src/Octoshift/Services/AdoApi.cs

Large diffs are not rendered by default.

262 changes: 254 additions & 8 deletions src/OctoshiftCLI.Tests/Octoshift/Services/AdoApiTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
Expand Down Expand Up @@ -940,16 +940,25 @@ public async Task GetPipeline_Should_Return_Pipeline()
defaultBranch,
clean,
checkoutSubmodules = default(object)
},
triggers = new[]
{
new
{
triggerType = "continuousIntegration",
branchFilters = new[] { "+refs/heads/main" }
}
}
};

_mockAdoClient.Setup(x => x.GetAsync(endpoint).Result).Returns(response.ToJson());

var (DefaultBranch, Clean, CheckoutSubmodules) = await sut.GetPipeline(ADO_ORG, ADO_TEAM_PROJECT, pipelineId);
var (DefaultBranch, Clean, CheckoutSubmodules, Triggers) = await sut.GetPipeline(ADO_ORG, ADO_TEAM_PROJECT, pipelineId);

DefaultBranch.Should().Be(branchName);
Clean.Should().Be("true");
CheckoutSubmodules.Should().Be("null");
Triggers.Should().NotBeNull();
}

[Fact]
Expand Down Expand Up @@ -1003,7 +1012,7 @@ public async Task ChangePipelineRepo_Should_Send_Correct_Payload()
refsUrl = $"https://api.github.com/repos/{GITHUB_ORG}/{githubRepo}/git/refs",
safeRepository = $"{GITHUB_ORG}/{githubRepo}",
shortName = githubRepo,
reportBuildStatus = true
reportBuildStatus = "true"
},
id = $"{GITHUB_ORG}/{githubRepo}",
type = "GitHub",
Expand All @@ -1013,11 +1022,39 @@ public async Task ChangePipelineRepo_Should_Send_Correct_Payload()
clean,
checkoutSubmodules
},
oneLastThing = false
oneLastThing = false,
triggers = new object[]
{
new
{
triggerType = "continuousIntegration",
settingsSourceType = 2,
branchFilters = Array.Empty<object>(),
pathFilters = Array.Empty<object>(),
batchChanges = false,
reportBuildStatus = "true"
},
new
{
triggerType = "pullRequest",
settingsSourceType = 2,
isCommentRequiredForPullRequest = false,
requireCommentsForNonTeamMembersOnly = false,
forks = new
{
enabled = false,
allowSecrets = false
},
branchFilters = Array.Empty<object>(),
pathFilters = Array.Empty<object>(),
reportBuildStatus = "true"
}
},
settingsSourceType = 2 // Use YAML definitions
};

_mockAdoClient.Setup(m => m.GetAsync(endpoint).Result).Returns(oldJson.ToJson());
await sut.ChangePipelineRepo(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, githubRepo, serviceConnectionId);
await sut.ChangePipelineRepo(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, githubRepo, serviceConnectionId, null, null);

_mockAdoClient.Verify(m => m.PutAsync(endpoint, It.Is<object>(y => y.ToJson() == newJson.ToJson())));
}
Expand Down Expand Up @@ -1085,7 +1122,7 @@ public async Task ChangePipelineRepo_Should_Send_Correct_Payload_With_TargetApiU
refsUrl,
safeRepository = $"{GITHUB_ORG}/{githubRepo}",
shortName = githubRepo,
reportBuildStatus = true
reportBuildStatus = "true"
},
id = $"{GITHUB_ORG}/{githubRepo}",
type = "GitHub",
Expand All @@ -1095,15 +1132,224 @@ public async Task ChangePipelineRepo_Should_Send_Correct_Payload_With_TargetApiU
clean,
checkoutSubmodules
},
oneLastThing = false,
triggers = new object[]
{
new
{
triggerType = "continuousIntegration",
settingsSourceType = 2,
branchFilters = Array.Empty<object>(),
pathFilters = Array.Empty<object>(),
batchChanges = false,
reportBuildStatus = "true"
},
new
{
triggerType = "pullRequest",
settingsSourceType = 2,
isCommentRequiredForPullRequest = false,
requireCommentsForNonTeamMembersOnly = false,
forks = new
{
enabled = false,
allowSecrets = false
},
branchFilters = Array.Empty<object>(),
pathFilters = Array.Empty<object>(),
reportBuildStatus = "true"
}
},
settingsSourceType = 2 // Use YAML definitions
};

_mockAdoClient.Setup(m => m.GetAsync(endpoint).Result).Returns(oldJson.ToJson());
await sut.ChangePipelineRepo(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, githubRepo, serviceConnectionId, null, targetApiUrl);

_mockAdoClient.Verify(m => m.PutAsync(endpoint, It.Is<object>(y => y.ToJson() == newJson.ToJson())));
}

[Fact]
public async Task ChangePipelineRepo_Should_Preserve_Triggers()
{
var githubRepo = "foo-repo";
var serviceConnectionId = Guid.NewGuid().ToString();
var defaultBranch = "foo-branch";
var pipelineId = 123;
var clean = "true";
var checkoutSubmodules = "false";

var originalTriggers = JArray.Parse(@"[
{
'triggerType': 'pullRequest',
'forks': {
'enabled': true,
'allowSecrets': false
},
'branchFilters': ['+refs/heads/*']
}
]");

var oldJson = new
{
something = "foo",
repository = new
{
testing = true
},
triggers = new[]
{
new
{
triggerType = "continuousIntegration",
branchFilters = new[] { "+refs/heads/main" }
}
},
oneLastThing = false
};

var endpoint = $"https://dev.azure.com/{ADO_ORG.EscapeDataString()}/{ADO_TEAM_PROJECT.EscapeDataString()}/_apis/build/definitions/{pipelineId}?api-version=6.0";

var newJson = new
{
something = "foo",
repository = new
{
properties = new
{
apiUrl = $"https://api.github.com/repos/{GITHUB_ORG}/{githubRepo}",
branchesUrl = $"https://api.github.com/repos/{GITHUB_ORG}/{githubRepo}/branches",
cloneUrl = $"https://github.com/{GITHUB_ORG}/{githubRepo}.git",
connectedServiceId = serviceConnectionId,
defaultBranch,
fullName = $"{GITHUB_ORG}/{githubRepo}",
manageUrl = $"https://github.com/{GITHUB_ORG}/{githubRepo}",
orgName = GITHUB_ORG,
refsUrl = $"https://api.github.com/repos/{GITHUB_ORG}/{githubRepo}/git/refs",
safeRepository = $"{GITHUB_ORG}/{githubRepo}",
shortName = githubRepo,
reportBuildStatus = "true" // String to match other tests
},
id = $"{GITHUB_ORG}/{githubRepo}",
type = "GitHub",
name = $"{GITHUB_ORG}/{githubRepo}",
url = $"https://github.com/{GITHUB_ORG}/{githubRepo}.git",
defaultBranch,
clean,
checkoutSubmodules
},
triggers = new object[]
{
new
{
triggerType = "continuousIntegration",
settingsSourceType = 2,
branchFilters = Array.Empty<object>(),
pathFilters = Array.Empty<object>(),
batchChanges = false,
reportBuildStatus = "true"
},
new
{
triggerType = "pullRequest",
settingsSourceType = 2,
isCommentRequiredForPullRequest = false,
requireCommentsForNonTeamMembersOnly = false,
forks = new
{
enabled = false,
allowSecrets = false
},
branchFilters = Array.Empty<object>(),
pathFilters = Array.Empty<object>(),
reportBuildStatus = "true"
}
},
oneLastThing = false,
settingsSourceType = 2 // Use YAML definitions instead of UI override
};

_mockAdoClient.Setup(m => m.GetAsync(endpoint).Result).Returns(oldJson.ToJson());
await sut.ChangePipelineRepo(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, githubRepo, serviceConnectionId, targetApiUrl);
await sut.ChangePipelineRepo(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, githubRepo, serviceConnectionId, originalTriggers, null);

_mockAdoClient.Verify(m => m.PutAsync(endpoint, It.Is<object>(y => y.ToJson() == newJson.ToJson())));
}

[Fact]
public async Task ChangePipelineRepo_Should_Enhance_PullRequest_Validation()
{
var githubRepo = "foo-repo";
var serviceConnectionId = Guid.NewGuid().ToString();
var defaultBranch = "foo-branch";
var pipelineId = 123;
var clean = "true";
var checkoutSubmodules = "false";

// Original triggers with minimal PR trigger
var originalTriggers = JArray.Parse(@"[
{
'triggerType': 'continuousIntegration',
'branchFilters': ['+refs/heads/main']
},
{
'triggerType': 'pullRequest',
'branchFilters': ['+refs/heads/develop']
}
]");

var oldJson = new
{
something = "foo",
repository = new
{
testing = true
},
triggers = new[]
{
new
{
triggerType = "continuousIntegration",
branchFilters = new[] { "+refs/heads/main" }
},
new
{
triggerType = "pullRequest",
branchFilters = new[] { "+refs/heads/develop" }
}
},
oneLastThing = false
};

var endpoint = $"https://dev.azure.com/{ADO_ORG.EscapeDataString()}/{ADO_TEAM_PROJECT.EscapeDataString()}/_apis/build/definitions/{pipelineId}?api-version=6.0";

_mockAdoClient.Setup(m => m.GetAsync(endpoint).Result).Returns(oldJson.ToJson());
await sut.ChangePipelineRepo(ADO_ORG, ADO_TEAM_PROJECT, pipelineId, defaultBranch, clean, checkoutSubmodules, GITHUB_ORG, githubRepo, serviceConnectionId, originalTriggers, null);

// Verify the PUT call was made with enhanced triggers
_mockAdoClient.Verify(m => m.PutAsync(endpoint, It.Is<object>(payload =>
VerifyEnhancedPullRequestTriggers(payload))), Times.Once);
}

private static bool VerifyEnhancedPullRequestTriggers(object payload)
{
var json = JObject.Parse(payload.ToJson());

// Verify settingsSourceType is set to use YAML (2) instead of UI override (1)
if (json["settingsSourceType"]?.Value<int>() != 2)
{
return false;
}

if (json["triggers"] is not JArray triggers)
{
return false;
}

// For YAML-only approach, triggers should be empty array
// This ensures "Override the YAML pull request trigger from here" is unchecked
return triggers.Count == 0;
}

[Fact]
public async Task GetBoardsGithubRepoId_Should_Return_RepoId()
{
Expand Down Expand Up @@ -1543,7 +1789,7 @@ await FluentActions
[Fact]
public async Task CreateBoardsGithubConnection_Should_Throw_When_Response_Is_Malformed()
{
// Arrange
// Arrange
var endpoint = $"https://dev.azure.com/{ADO_ORG.EscapeDataString()}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1";
var malformedResponse = "{ invalid json";

Expand Down
Loading
Loading