Skip to content

Commit

Permalink
ado-team-project option on generate-script (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
GordonBeeming authored Mar 30, 2022
1 parent cb69118 commit 2285eed
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 38 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,4 @@ MigrationBackup/
/dist
/src/gei/Properties/launchSettings.json
/src/OctoshiftCLI.IntegrationTests/Properties/launchSettings.json
/src/ado2gh/Properties/launchSettings.json
3 changes: 2 additions & 1 deletion RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

- Add ado-team-project parameter to `ado2gh generate-script` command
- Add ado-team-project parameter to `gh gei generate-script` command
22 changes: 14 additions & 8 deletions src/Octoshift/AdoApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,28 @@ public virtual async Task<string> GetGithubAppId(string org, string githubOrg, I
return null;
}

foreach (var adoTeamProject in teamProjects)
foreach (var teamProject in teamProjects)
{
var url = $"https://dev.azure.com/{org}/{adoTeamProject}/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4";
var response = await _client.GetWithPagingAsync(url);

var endpoint = response.FirstOrDefault(x => (string)x["type"] == "GitHub" && ((string)x["name"]).ToLower() == githubOrg.ToLower());

if (endpoint != null)
var appId = await GetTeamProjectGithubAppId(org, githubOrg, teamProject);
if (appId != null)
{
return (string)endpoint["id"];
return appId;
}
}

return null;
}

private async Task<string> GetTeamProjectGithubAppId(string org, string githubOrg, string teamProject)
{
var url = $"https://dev.azure.com/{org}/{teamProject}/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4";
var response = await _client.GetWithPagingAsync(url);

var endpoint = response.FirstOrDefault(x => ((string)x["type"]).Equals("GitHub", StringComparison.OrdinalIgnoreCase) && ((string)x["name"]).Equals(githubOrg, StringComparison.OrdinalIgnoreCase));

return endpoint != null ? (string)endpoint["id"] : null;
}

public virtual async Task<string> GetGithubHandle(string org, string orgId, string teamProject, string githubToken)
{
var url = $"https://dev.azure.com/{org}/_apis/Contribution/HierarchyQuery?api-version=5.0-preview.1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ public void Should_Have_Options()
var command = new GenerateScriptCommand(null, null);
command.Should().NotBeNull();
command.Name.Should().Be("generate-script");
command.Options.Count.Should().Be(8);
command.Options.Count.Should().Be(9);

TestHelpers.VerifyCommandOption(command.Options, "github-org", true);
TestHelpers.VerifyCommandOption(command.Options, "ado-org", false);
TestHelpers.VerifyCommandOption(command.Options, "ado-team-project", false);
TestHelpers.VerifyCommandOption(command.Options, "output", false);
TestHelpers.VerifyCommandOption(command.Options, "repos-only", false);
TestHelpers.VerifyCommandOption(command.Options, "skip-idp", false);
Expand Down Expand Up @@ -353,6 +354,7 @@ public async Task GetRepos_Two_Repos_Two_Team_Projects()
{
var org = "foo-org";
var orgs = new List<string>() { org };
var teamProject = string.Empty;
var teamProject1 = "foo-tp1";
var teamProject2 = "foo-tp2";
var teamProjects = new List<string>() { teamProject1, teamProject2 };
Expand All @@ -366,14 +368,64 @@ public async Task GetRepos_Two_Repos_Two_Team_Projects()
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject2).Result).Returns(new List<string>() { repo2 });

var command = new GenerateScriptCommand(new Mock<OctoLogger>().Object, null);
var result = await command.GetRepos(mockAdo.Object, orgs);
var result = await command.GetRepos(mockAdo.Object, orgs, teamProject);

Assert.Single(result[org][teamProject1]);
Assert.Single(result[org][teamProject2]);
Assert.Contains(result[org][teamProject1], x => x == repo1);
Assert.Contains(result[org][teamProject2], x => x == repo2);
}

[Fact]
public async Task GetRepos_Two_Repos_Two_Team_Projects_With_Team_Project_Supplied()
{
var org = "foo-org";
var orgs = new List<string>() { org };
var teamProject1 = "foo-tp1";
var teamProject2 = "foo-tp2";
var teamProjectArg = teamProject1;
var teamProjects = new List<string>() { teamProject1, teamProject2 };
var repo1 = "foo-repo1";
var repo2 = "foo-repo2";

var mockAdo = new Mock<AdoApi>(null);

mockAdo.Setup(x => x.GetTeamProjects(org).Result).Returns(teamProjects);
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject1).Result).Returns(new List<string>() { repo1 });
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject2).Result).Returns(new List<string>() { repo2 });

var command = new GenerateScriptCommand(new Mock<OctoLogger>().Object, null);
var result = await command.GetRepos(mockAdo.Object, orgs, teamProjectArg);

Assert.Single(result[org][teamProjectArg]);
Assert.False(result[org].ContainsKey(teamProject2));
Assert.Contains(result[org][teamProjectArg], x => x == repo1);
}

[Fact]
public async Task GetRepos_With_Team_Project_Supplied_Does_Not_Exist()
{
var org = "foo-org";
var orgs = new List<string>() { org };
var teamProject1 = "foo-tp1";
var teamProject2 = "foo-tp2";
var teamProjectArg = "foo-tp3";
var teamProjects = new List<string>() { teamProject1, teamProject2 };
var repo1 = "foo-repo1";
var repo2 = "foo-repo2";

var mockAdo = new Mock<AdoApi>(null);

mockAdo.Setup(x => x.GetTeamProjects(org).Result).Returns(teamProjects);
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject1).Result).Returns(new List<string>() { repo1 });
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject2).Result).Returns(new List<string>() { repo2 });

var command = new GenerateScriptCommand(new Mock<OctoLogger>().Object, null);
var result = await command.GetRepos(mockAdo.Object, orgs, teamProjectArg);

Assert.Empty(result[org].Keys);
}

[Fact]
public async Task GetPipelines_One_Repo_Two_Pipelines()
{
Expand Down Expand Up @@ -401,6 +453,26 @@ public async Task GetPipelines_One_Repo_Two_Pipelines()
Assert.Contains(result[org][teamProject][repo], x => x == pipeline2);
}

[Fact]
public async Task GetAppIds_With_Team_Project_Supplied_Does_Not_Exist()
{
var org = "foo-org";
var orgs = new List<string>() { org };
var githubOrg = "foo-gh-org";
var teamProject1 = "foo-tp1";
var teamProject2 = "foo-tp2";
var teamProjects = new List<string>() { teamProject1, teamProject2 };

var mockAdo = new Mock<AdoApi>(null);

mockAdo.Setup(x => x.GetTeamProjects(org).Result).Returns(teamProjects);

var command = new GenerateScriptCommand(new Mock<OctoLogger>().Object, null);
var result = await command.GetAppIds(mockAdo.Object, orgs, githubOrg);

Assert.Empty(result);
}

[Fact]
public async Task GetAppIds_Service_Connect_Exists()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using OctoshiftCLI.GithubEnterpriseImporter.Commands;
Expand All @@ -21,10 +22,11 @@ public void Should_Have_Options()

command.Should().NotBeNull();
command.Name.Should().Be("generate-script");
command.Options.Count.Should().Be(10);
command.Options.Count.Should().Be(11);

TestHelpers.VerifyCommandOption(command.Options, "github-source-org", false);
TestHelpers.VerifyCommandOption(command.Options, "ado-source-org", false);
TestHelpers.VerifyCommandOption(command.Options, "ado-team-project", false);
TestHelpers.VerifyCommandOption(command.Options, "github-target-org", true);
TestHelpers.VerifyCommandOption(command.Options, "ghes-api-url", false);
TestHelpers.VerifyCommandOption(command.Options, "azure-storage-connection-string", false);
Expand Down Expand Up @@ -95,6 +97,80 @@ public void Github_Multiple_Repos()
script.Should().Be(expected);
}

[Fact]
public async Task GetRepos_Two_Repos_Two_Team_Projects()
{
var org = "foo-org";
var teamProject = string.Empty;
var teamProject1 = "foo-tp1";
var teamProject2 = "foo-tp2";
var teamProjects = new List<string>() { teamProject1, teamProject2 };
var repo1 = "foo-repo1";
var repo2 = "foo-repo2";

var mockAdo = new Mock<AdoApi>(null);

mockAdo.Setup(x => x.GetTeamProjects(org).Result).Returns(teamProjects);
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject1).Result).Returns(new List<string>() { repo1 });
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject2).Result).Returns(new List<string>() { repo2 });

var command = new GenerateScriptCommand(new Mock<OctoLogger>().Object, null, null, null);
var result = await command.GetAdoRepos(mockAdo.Object, org, teamProject);

Assert.Single(result[teamProject1]);
Assert.Single(result[teamProject2]);
Assert.Contains(result[teamProject1], x => x == repo1);
Assert.Contains(result[teamProject2], x => x == repo2);
}

[Fact]
public async Task GetRepos_Two_Repos_Two_Team_Projects_With_Team_Project_Supplied()
{
var org = "foo-org";
var teamProject1 = "foo-tp1";
var teamProject2 = "foo-tp2";
var teamProjectArg = teamProject1;
var teamProjects = new List<string>() { teamProject1, teamProject2 };
var repo1 = "foo-repo1";
var repo2 = "foo-repo2";

var mockAdo = new Mock<AdoApi>(null);

mockAdo.Setup(x => x.GetTeamProjects(org).Result).Returns(teamProjects);
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject1).Result).Returns(new List<string>() { repo1 });
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject2).Result).Returns(new List<string>() { repo2 });

var command = new GenerateScriptCommand(new Mock<OctoLogger>().Object, null, null, null);
var result = await command.GetAdoRepos(mockAdo.Object, org, teamProjectArg);

Assert.Single(result[teamProjectArg]);
Assert.False(result.ContainsKey(teamProject2));
Assert.Contains(result[teamProjectArg], x => x == repo1);
}

[Fact]
public async Task GetRepos_With_Team_Project_Supplied_Does_Not_Exist()
{
var org = "foo-org";
var teamProject1 = "foo-tp1";
var teamProject2 = "foo-tp2";
var teamProjectArg = "foo-tp3";
var teamProjects = new List<string>() { teamProject1, teamProject2 };
var repo1 = "foo-repo1";
var repo2 = "foo-repo2";

var mockAdo = new Mock<AdoApi>(null);

mockAdo.Setup(x => x.GetTeamProjects(org).Result).Returns(teamProjects);
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject1).Result).Returns(new List<string>() { repo1 });
mockAdo.Setup(x => x.GetEnabledRepos(org, teamProject2).Result).Returns(new List<string>() { repo2 });

var command = new GenerateScriptCommand(new Mock<OctoLogger>().Object, null, null, null);
var result = await command.GetAdoRepos(mockAdo.Object, org, teamProjectArg);

Assert.Empty(result);
}

[Fact]
public void Github_GHES_Repo()
{
Expand Down
3 changes: 2 additions & 1 deletion src/OctoshiftCLI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7AE2DD08-9B89-4B29-89B1-AEFA50169EE3}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
..\RELEASENOTES.md = ..\RELEASENOTES.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octoshift", "Octoshift\Octoshift.csproj", "{FA310CCF-3FF2-4DED-9619-28B6DBC3DC4F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "gei", "gei\gei.csproj", "{0818E9ED-2B8A-4312-A3B2-6E1B1ED78A1A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OctoshiftCLI.IntegrationTests", "OctoshiftCLI.IntegrationTests\OctoshiftCLI.IntegrationTests.csproj", "{308F794A-92E9-43E1-A092-C2F7D99F2C44}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OctoshiftCLI.IntegrationTests", "OctoshiftCLI.IntegrationTests\OctoshiftCLI.IntegrationTests.csproj", "{308F794A-92E9-43E1-A092-C2F7D99F2C44}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
53 changes: 41 additions & 12 deletions src/ado2gh/Commands/GenerateScriptCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public GenerateScriptCommand(OctoLogger log, AdoApiFactory adoApiFactory) : base
{
IsRequired = false
};
var adoTeamProject = new Option<string>("--ado-team-project")
{
IsRequired = false
};
var outputOption = new Option<FileInfo>("--output", () => new FileInfo("./migrate.ps1"))
{
IsRequired = false
Expand Down Expand Up @@ -62,23 +66,25 @@ public GenerateScriptCommand(OctoLogger log, AdoApiFactory adoApiFactory) : base

AddOption(githubOrgOption);
AddOption(adoOrgOption);
AddOption(adoTeamProject);
AddOption(outputOption);
AddOption(reposOnlyOption);
AddOption(skipIdpOption);
AddOption(sshOption);
AddOption(sequential);
AddOption(verbose);

Handler = CommandHandler.Create<string, string, FileInfo, bool, bool, bool, bool, bool>(Invoke);
Handler = CommandHandler.Create<string, string, string, FileInfo, bool, bool, bool, bool, bool>(Invoke);
}

public async Task Invoke(string githubOrg, string adoOrg, FileInfo output, bool reposOnly, bool skipIdp, bool ssh = false, bool sequential = false, bool verbose = false)
public async Task Invoke(string githubOrg, string adoOrg, string adoTeamProject, FileInfo output, bool reposOnly, bool skipIdp, bool ssh = false, bool sequential = false, bool verbose = false)
{
_log.Verbose = verbose;

_log.LogInformation("Generating Script...");
_log.LogInformation($"GITHUB ORG: {githubOrg}");
_log.LogInformation($"ADO ORG: {adoOrg}");
_log.LogInformation($"ADO TEAM PROJECT: {adoTeamProject}");
_log.LogInformation($"OUTPUT: {output}");
if (ssh)
{
Expand All @@ -94,7 +100,7 @@ public async Task Invoke(string githubOrg, string adoOrg, FileInfo output, bool
var ado = _adoApiFactory.Create();

var orgs = await GetOrgs(ado, adoOrg);
var repos = await GetRepos(ado, orgs);
var repos = await GetRepos(ado, orgs, adoTeamProject);
var pipelines = _reposOnly ? null : await GetPipelines(ado, repos);
var appIds = _reposOnly ? null : await GetAppIds(ado, orgs, githubOrg);

Expand Down Expand Up @@ -163,36 +169,59 @@ public async Task<IDictionary<string, IDictionary<string, IDictionary<string, IE
return pipelines;
}

public async Task<IDictionary<string, IDictionary<string, IEnumerable<string>>>> GetRepos(AdoApi ado, IEnumerable<string> orgs)
public async Task<IDictionary<string, IDictionary<string, IEnumerable<string>>>> GetRepos(AdoApi ado, IEnumerable<string> orgs, string adoTeamProject)
{
var repos = new Dictionary<string, IDictionary<string, IEnumerable<string>>>();

if (orgs != null && ado != null)
{
var teamProjectExists = false;
foreach (var org in orgs)
{
_log.LogInformation($"ADO ORG: {org}");
repos.Add(org, new Dictionary<string, IEnumerable<string>>());

var teamProjects = await ado.GetTeamProjects(org);

foreach (var teamProject in teamProjects)
if (string.IsNullOrEmpty(adoTeamProject))
{
_log.LogInformation($" Team Project: {teamProject}");
var projectRepos = await ado.GetEnabledRepos(org, teamProject);
repos[org].Add(teamProject, projectRepos);

foreach (var repo in projectRepos)
foreach (var teamProject in teamProjects)
{
teamProjectExists = true;
var projectRepos = await GetTeamProjectRepos(ado, org, teamProject);
repos[org].Add(teamProject, projectRepos);
}
}
else
{
if (teamProjects.Any(o => o.Equals(adoTeamProject, StringComparison.OrdinalIgnoreCase)))
{
_log.LogInformation($" Repo: {repo}");
teamProjectExists = true;
var projectRepos = await GetTeamProjectRepos(ado, org, adoTeamProject);
repos[org].Add(adoTeamProject, projectRepos);
}
}
}
if (!teamProjectExists)
{
_log.LogWarning($"ADO Team Project provided cannot be found [{adoTeamProject}]");
}
}

return repos;
}

private async Task<IEnumerable<string>> GetTeamProjectRepos(AdoApi ado, string org, string teamProject)
{
_log.LogInformation($" Team Project: {teamProject}");
var projectRepos = await ado.GetEnabledRepos(org, teamProject);

foreach (var repo in projectRepos)
{
_log.LogInformation($" Repo: {repo}");
}
return projectRepos;
}

public async Task<IEnumerable<string>> GetOrgs(AdoApi ado, string adoOrg)
{
var orgs = new List<string>();
Expand Down
Loading

0 comments on commit 2285eed

Please sign in to comment.