Skip to content

Commit 9a46741

Browse files
authored
Add --repo-list to generate-script (#468)
1 parent 61b08e2 commit 9a46741

File tree

9 files changed

+296
-23
lines changed

9 files changed

+296
-23
lines changed

RELEASENOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- added `--repo-list` argument to `ado2gh generate-script`. This accepts a repos.csv file previously generated by `ado2gh inventory-report`. This can be used to split a large migration up into batches of repos.
12
- `--github-pat` arg in the `gei reclaim-mannequin` command was renamed to `--github-target-pat` to follow the same naming convention for other commands like ` gei migrate-repo` or `gei generate-mannequin-csv`
23
- `ado2gh inventory-report` command now also reports the compressed size of each repo in `repos.csv`.
34
- fixed bug in `gh gei generate-script` so that it properly respects the `--no-ssl-verify` argument.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[*.cs]
2+
3+
# CA1707: Identifiers should not contain underscores
4+
dotnet_diagnostic.CA1707.severity = none
5+
6+
# Naming rules
7+
8+
dotnet_naming_rule.public_methods_should_be_snake_case.severity = suggestion
9+
dotnet_naming_rule.public_methods_should_be_snake_case.symbols = public_methods
10+
dotnet_naming_rule.public_methods_should_be_snake_case.style = snake_case
11+
12+
# Symbol specifications
13+
14+
dotnet_naming_symbols.public_methods.applicable_kinds = method
15+
dotnet_naming_symbols.public_methods.applicable_accessibilities = public
16+
17+
# Naming styles
18+
19+
dotnet_naming_style.snake_case.capitalization = pascal_case
20+
dotnet_naming_style.snake_case.word_separator = _
21+
dotnet_naming_style.snake_case.required_prefix =
22+
dotnet_naming_style.snake_case.required_suffix =

src/OctoshiftCLI.IntegrationTests/AdoToGithub.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.IO;
34
using System.Net.Http;
45
using System.Threading.Tasks;
56
using Xunit;
@@ -48,6 +49,66 @@ public AdoToGithub(ITestOutputHelper output)
4849
_helper = new TestHelper(_output, adoApi, githubApi, adoClient, githubClient);
4950
}
5051

52+
[Fact]
53+
public async Task With_Inventory_Report_Csv()
54+
{
55+
var adoOrg = $"gei-e2e-testing-{TestHelper.GetOsName()}";
56+
var githubOrg = $"e2e-testing-{TestHelper.GetOsName()}";
57+
var teamProject1 = "gei-e2e-1";
58+
var teamProject2 = "gei-e2e-2";
59+
var adoRepo1 = teamProject1;
60+
var adoRepo2 = teamProject2;
61+
var pipeline1 = "pipeline1";
62+
var pipeline2 = "pipeline2";
63+
64+
await _helper.ResetAdoTestEnvironment(adoOrg);
65+
await _helper.ResetGithubTestEnvironment(githubOrg);
66+
67+
await _helper.CreateTeamProject(adoOrg, teamProject1);
68+
var commitId = await _helper.InitializeAdoRepo(adoOrg, teamProject1, adoRepo1);
69+
await _helper.CreatePipeline(adoOrg, teamProject1, adoRepo1, pipeline1, commitId);
70+
71+
await _helper.CreateTeamProject(adoOrg, teamProject2);
72+
commitId = await _helper.InitializeAdoRepo(adoOrg, teamProject2, adoRepo2);
73+
await _helper.CreatePipeline(adoOrg, teamProject2, adoRepo2, pipeline2, commitId);
74+
75+
await _helper.RunCliCommand($"inventory-report --ado-org {adoOrg}", Path.Join(TestHelper.GetOsDistPath(), "ado2gh"), _tokens);
76+
await _helper.RunAdoToGithubCliMigration($"generate-script --github-org {githubOrg} --ado-org {adoOrg} --all --repo-list repos.csv", _tokens);
77+
78+
_helper.AssertNoErrorInLogs(_startTime);
79+
80+
await _helper.AssertGithubRepoExists(githubOrg, $"{teamProject1}-{teamProject1}");
81+
await _helper.AssertGithubRepoExists(githubOrg, $"{teamProject2}-{teamProject2}");
82+
await _helper.AssertGithubRepoInitialized(githubOrg, $"{teamProject1}-{teamProject1}");
83+
await _helper.AssertGithubRepoInitialized(githubOrg, $"{teamProject2}-{teamProject2}");
84+
await _helper.AssertAutolinkConfigured(githubOrg, $"{teamProject1}-{teamProject1}", $"https://dev.azure.com/{adoOrg}/{teamProject1}/_workitems/edit/<num>/");
85+
await _helper.AssertAutolinkConfigured(githubOrg, $"{teamProject2}-{teamProject2}", $"https://dev.azure.com/{adoOrg}/{teamProject2}/_workitems/edit/<num>/");
86+
await _helper.AssertAdoRepoDisabled(adoOrg, teamProject1, adoRepo1);
87+
await _helper.AssertAdoRepoDisabled(adoOrg, teamProject2, adoRepo2);
88+
await _helper.AssertAdoRepoLocked(adoOrg, teamProject1, adoRepo1);
89+
await _helper.AssertAdoRepoLocked(adoOrg, teamProject2, adoRepo2);
90+
await _helper.AssertGithubTeamCreated(githubOrg, $"{teamProject1}-Maintainers");
91+
await _helper.AssertGithubTeamCreated(githubOrg, $"{teamProject1}-Admins");
92+
await _helper.AssertGithubTeamCreated(githubOrg, $"{teamProject2}-Maintainers");
93+
await _helper.AssertGithubTeamCreated(githubOrg, $"{teamProject2}-Admins");
94+
await _helper.AssertGithubTeamIdpLinked(githubOrg, $"{teamProject1}-Maintainers", $"{teamProject1}-Maintainers");
95+
await _helper.AssertGithubTeamIdpLinked(githubOrg, $"{teamProject1}-Admins", $"{teamProject1}-Admins");
96+
await _helper.AssertGithubTeamIdpLinked(githubOrg, $"{teamProject2}-Maintainers", $"{teamProject2}-Maintainers");
97+
await _helper.AssertGithubTeamIdpLinked(githubOrg, $"{teamProject2}-Admins", $"{teamProject2}-Admins");
98+
await _helper.AssertGithubTeamHasRepoRole(githubOrg, $"{teamProject1}-Maintainers", $"{teamProject1}-{teamProject1}", "maintain");
99+
await _helper.AssertGithubTeamHasRepoRole(githubOrg, $"{teamProject1}-Admins", $"{teamProject1}-{teamProject1}", "admin");
100+
await _helper.AssertGithubTeamHasRepoRole(githubOrg, $"{teamProject2}-Maintainers", $"{teamProject2}-{teamProject2}", "maintain");
101+
await _helper.AssertGithubTeamHasRepoRole(githubOrg, $"{teamProject2}-Admins", $"{teamProject2}-{teamProject2}", "admin");
102+
await _helper.AssertServiceConnectionWasShared(adoOrg, teamProject1);
103+
await _helper.AssertServiceConnectionWasShared(adoOrg, teamProject2);
104+
await _helper.AssertPipelineRewired(adoOrg, teamProject1, pipeline1, githubOrg, $"{teamProject1}-{teamProject1}");
105+
await _helper.AssertPipelineRewired(adoOrg, teamProject2, pipeline2, githubOrg, $"{teamProject2}-{teamProject2}");
106+
await _helper.AssertBoardsIntegrationConfigured(adoOrg, teamProject1);
107+
await _helper.AssertBoardsIntegrationConfigured(adoOrg, teamProject2);
108+
_helper.AssertMigrationLogFileExists(githubOrg, $"{teamProject1}-{teamProject1}");
109+
_helper.AssertMigrationLogFileExists(githubOrg, $"{teamProject2}-{teamProject2}");
110+
}
111+
51112
[Fact]
52113
public async Task Basic()
53114
{

src/OctoshiftCLI.IntegrationTests/TestHelper.cs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,19 @@ public static string GetOsName()
426426

427427
public async Task RunCliMigration(string generateScriptCommand, string cliName, IDictionary<string, string> tokens)
428428
{
429+
await RunCliCommand(generateScriptCommand, cliName, tokens);
430+
await RunPowershellScript("migrate.ps1", tokens);
431+
}
432+
433+
public async Task RunPowershellScript(string script, IDictionary<string, string> tokens)
434+
{
435+
var scriptPath = Path.Join(GetOsDistPath(), script);
436+
429437
var startInfo = new ProcessStartInfo
430438
{
431439
WorkingDirectory = GetOsDistPath(),
432-
FileName = $"{cliName}",
433-
Arguments = generateScriptCommand
440+
FileName = "pwsh",
441+
Arguments = $"-File {scriptPath}"
434442
};
435443

436444
if (tokens != null)
@@ -449,20 +457,42 @@ public async Task RunCliMigration(string generateScriptCommand, string cliName,
449457
}
450458

451459
_output.WriteLine($"Running command: {startInfo.FileName} {startInfo.Arguments}");
460+
452461
var p = Process.Start(startInfo);
453462
await p.WaitForExitAsync();
454463

455-
p.ExitCode.Should().Be(0, "generate-script should return an exit code of 0");
464+
p.ExitCode.Should().Be(0, $"{script} should return an exit code of 0");
465+
}
456466

457-
startInfo.FileName = "pwsh";
458-
var scriptPath = Path.Join(startInfo.WorkingDirectory, "migrate.ps1");
459-
startInfo.Arguments = $"-File {scriptPath}";
467+
public async Task RunCliCommand(string command, string cliName, IDictionary<string, string> tokens)
468+
{
469+
var startInfo = new ProcessStartInfo
470+
{
471+
WorkingDirectory = GetOsDistPath(),
472+
FileName = cliName,
473+
Arguments = command
474+
};
475+
476+
if (tokens != null)
477+
{
478+
foreach (var token in tokens)
479+
{
480+
if (startInfo.EnvironmentVariables.ContainsKey(token.Key))
481+
{
482+
startInfo.EnvironmentVariables[token.Key] = token.Value;
483+
}
484+
else
485+
{
486+
startInfo.EnvironmentVariables.Add(token.Key, token.Value);
487+
}
488+
}
489+
}
460490

461491
_output.WriteLine($"Running command: {startInfo.FileName} {startInfo.Arguments}");
462-
p = Process.Start(startInfo);
492+
var p = Process.Start(startInfo);
463493
await p.WaitForExitAsync();
464494

465-
p.ExitCode.Should().Be(0, "migrate.ps1 should return an exit code of 0");
495+
p.ExitCode.Should().Be(0, $"{cliName} should return an exit code of 0");
466496
}
467497

468498
public async Task RunAdoToGithubCliMigration(string generateScriptCommand, IDictionary<string, string> tokens) =>
@@ -471,7 +501,7 @@ public async Task RunAdoToGithubCliMigration(string generateScriptCommand, IDict
471501
public async Task RunGeiCliMigration(string generateScriptCommand, IDictionary<string, string> tokens) =>
472502
await RunCliMigration($"gei {generateScriptCommand}", "gh", tokens);
473503

474-
private static string GetOsDistPath()
504+
public static string GetOsDistPath()
475505
{
476506
return RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
477507
? Path.Join(Directory.GetCurrentDirectory(), "../../../../../dist/linux-x64")

src/OctoshiftCLI.Tests/TestExtensionMethods.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Collections.Generic;
2+
using System.IO;
23
using System.Linq;
4+
using System.Text;
35
using Newtonsoft.Json.Linq;
46

57
namespace OctoshiftCLI.Tests
@@ -8,5 +10,10 @@ public static class TestExtensionMethods
810
{
911
public static IAsyncEnumerable<JToken> ToAsyncJTokenEnumerable<T>(this IEnumerable<T> list)
1012
=> list.Select(x => JToken.FromObject(x)).ToAsyncEnumerable();
13+
14+
public static MemoryStream ToStream(this string value)
15+
{
16+
return new MemoryStream(Encoding.UTF8.GetBytes(value ?? ""));
17+
}
1118
}
1219
}

src/OctoshiftCLI.Tests/ado2gh/Commands/GenerateScriptCommandTests.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public void Should_Have_Options()
6262
var command = new GenerateScriptCommand(null, null, null, null);
6363
command.Should().NotBeNull();
6464
command.Name.Should().Be("generate-script");
65-
command.Options.Count.Should().Be(16);
65+
command.Options.Count.Should().Be(17);
6666

6767
TestHelpers.VerifyCommandOption(command.Options, "github-org", true);
6868
TestHelpers.VerifyCommandOption(command.Options, "ado-org", false);
@@ -80,6 +80,7 @@ public void Should_Have_Options()
8080
TestHelpers.VerifyCommandOption(command.Options, "integrate-boards", false);
8181
TestHelpers.VerifyCommandOption(command.Options, "rewire-pipelines", false);
8282
TestHelpers.VerifyCommandOption(command.Options, "all", false);
83+
TestHelpers.VerifyCommandOption(command.Options, "repo-list", false);
8384
}
8485

8586
[Fact]
@@ -131,6 +132,38 @@ public async Task SequentialScript_Single_Repo_No_Options()
131132
_scriptOutput.Should().Be(expected);
132133
}
133134

135+
[Fact]
136+
public async Task SequentialScript_With_RepoList()
137+
{
138+
// Arrange
139+
var repoList = new FileInfo("repos.csv");
140+
141+
_mockAdoApiFactory.Setup(m => m.Create(null)).Returns(_mockAdoApi.Object);
142+
143+
_mockAdoInspector.Setup(m => m.GetRepoCount()).ReturnsAsync(1);
144+
_mockAdoInspector.Setup(m => m.GetOrgs()).ReturnsAsync(ADO_ORGS);
145+
_mockAdoInspector.Setup(m => m.GetTeamProjects(ADO_ORG)).ReturnsAsync(ADO_TEAM_PROJECTS);
146+
_mockAdoInspector.Setup(m => m.GetRepos(ADO_ORG, ADO_TEAM_PROJECT)).ReturnsAsync(ADO_REPOS);
147+
148+
// Act
149+
var args = new GenerateScriptCommandArgs
150+
{
151+
GithubOrg = GITHUB_ORG,
152+
AdoOrg = ADO_ORG,
153+
Sequential = true,
154+
Output = new FileInfo("unit-test-output"),
155+
RepoList = repoList
156+
};
157+
await _command.Invoke(args);
158+
159+
_scriptOutput = TrimNonExecutableLines(_scriptOutput);
160+
var expected = $"Exec {{ ./ado2gh migrate-repo --ado-org \"{ADO_ORG}\" --ado-team-project \"{ADO_TEAM_PROJECT}\" --ado-repo \"{FOO_REPO}\" --github-org \"{GITHUB_ORG}\" --github-repo \"{ADO_TEAM_PROJECT}-{FOO_REPO}\" --wait }}";
161+
162+
// Assert
163+
_scriptOutput.Should().Be(expected);
164+
_mockAdoInspector.Verify(m => m.LoadReposCsv(repoList.FullName));
165+
}
166+
134167
[Fact]
135168
public async Task SequentialScript_Single_Repo_All_Options()
136169
{

src/OctoshiftCLI.Tests/ado2gh/Services/AdoInspectorServiceTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,53 @@ public async Task GetPipelines_Should_Return_All_Pipelines()
120120
// Assert
121121
result.Should().BeEquivalentTo(pipelines);
122122
}
123+
124+
[Fact]
125+
public async Task LoadReposCsv_Should_Set_Orgs()
126+
{
127+
// Arrange
128+
var csvPath = "repos.csv";
129+
var csvContents = $"org,teamproject,repo{Environment.NewLine}\"{ADO_ORG}\",\"{ADO_TEAM_PROJECT}\",\"{FOO_REPO}\"";
130+
131+
_service.OpenFileStream = _ => csvContents.ToStream();
132+
133+
// Act
134+
_service.LoadReposCsv(csvPath);
135+
136+
// Assert
137+
(await _service.GetOrgs()).Should().BeEquivalentTo(new List<string>() { ADO_ORG });
138+
}
139+
140+
[Fact]
141+
public async Task LoadReposCsv_Should_Set_TeamProjects()
142+
{
143+
// Arrange
144+
var csvPath = "repos.csv";
145+
var csvContents = $"org,teamproject,repo{Environment.NewLine}\"{ADO_ORG}\",\"{ADO_TEAM_PROJECT}\",\"{FOO_REPO}\"";
146+
147+
_service.OpenFileStream = _ => csvContents.ToStream();
148+
149+
// Act
150+
_service.LoadReposCsv(csvPath);
151+
152+
// Assert
153+
(await _service.GetTeamProjects(ADO_ORG)).Should().BeEquivalentTo(new List<string>() { ADO_TEAM_PROJECT });
154+
}
155+
156+
[Fact]
157+
public async Task LoadReposCsv_Should_Set_Repos()
158+
{
159+
// Arrange
160+
var csvPath = "repos.csv";
161+
var csvContents = $"org,teamproject,repo{Environment.NewLine}\"{ADO_ORG}\",\"{ADO_TEAM_PROJECT}\",\"{FOO_REPO}\"";
162+
163+
_service.OpenFileStream = _ => csvContents.ToStream();
164+
165+
// Act
166+
_service.LoadReposCsv(csvPath);
167+
168+
// Assert
169+
(await _service.GetRepos(ADO_ORG, ADO_TEAM_PROJECT)).Single().Name.Should().Be(FOO_REPO);
170+
}
123171
}
124172
}

src/ado2gh/Commands/GenerateScriptCommand.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ public GenerateScriptCommand(OctoLogger log, AdoApiFactory adoApiFactory, IVersi
108108
IsRequired = false,
109109
Description = "Includes all script generation options."
110110
};
111+
var repoList = new Option<FileInfo>("--repo-list")
112+
{
113+
IsRequired = false,
114+
Description = "Path to a csv file that contains a list of repos to generate a script for. The CSV file should be generated using the inventory-report command."
115+
};
111116

112117
AddOption(githubOrgOption);
113118
AddOption(adoOrgOption);
@@ -125,6 +130,7 @@ public GenerateScriptCommand(OctoLogger log, AdoApiFactory adoApiFactory, IVersi
125130
AddOption(integrateBoards);
126131
AddOption(rewirePipelines);
127132
AddOption(all);
133+
AddOption(repoList);
128134

129135
Handler = CommandHandler.Create<GenerateScriptCommandArgs>(Invoke);
130136
}
@@ -158,6 +164,12 @@ public async Task Invoke(GenerateScriptCommandArgs args)
158164
_adoInspectorService.OrgFilter = args.AdoOrg;
159165
_adoInspectorService.TeamProjectFilter = args.AdoTeamProject;
160166

167+
if (args.RepoList.HasValue())
168+
{
169+
_log.LogInformation($"Loading Repo CSV File...");
170+
_adoInspectorService.LoadReposCsv(args.RepoList.FullName);
171+
}
172+
161173
if (await _adoInspectorService.GetRepoCount() == 0)
162174
{
163175
_log.LogError("A migration script could not be generated because no migratable repos were found. Please note that the GEI does not migrate disabled or TFVC repos.");
@@ -564,6 +576,10 @@ private void LogOptions(GenerateScriptCommandArgs args)
564576
{
565577
_log.LogInformation("ALL: true");
566578
}
579+
if (args.RepoList.HasValue())
580+
{
581+
_log.LogInformation($"REPO LIST: {args.RepoList}");
582+
}
567583
}
568584

569585
private class GenerateScriptOptions
@@ -638,5 +654,6 @@ public class GenerateScriptCommandArgs
638654
public bool IntegrateBoards { get; set; }
639655
public bool RewirePipelines { get; set; }
640656
public bool All { get; set; }
657+
public FileInfo RepoList { get; set; }
641658
}
642659
}

0 commit comments

Comments
 (0)