Skip to content

Commit 983803d

Browse files
authored
Import BBS archive with migrate-repo command (#582)
Closes #528! This PR updates the placeholder migrate-repo command for bbs2gh with real logic to ingest an archive URL and kick off a migration with Octoshift. A few notes about this PR: There are some parameters that pass strings like "not-used" to satisfy required parameters in GraphQL and Octoshift. When github/octoshift#5350 is shipped, these "filler" strings can be removed. CreateBbsMigrationSource was added to GithubApi. This is consumed by the bbs2gh MigrateRepoCommand to kick off migrations over GraphQL. bbs2gh.csproj was added as a reference to OctoshiftCLI.Tests.csproj. This is necessary to test classes in the OctoshiftCLI.BbsToGithub.Commands namespace. I opted to use a MigrateRepoCommandArgs class in bbs2gh's MigrateRepoCommand because I felt that it was cleaner and easier to read. Some unused ADO logic was pulled from bbs2gh's EnvironmentVariableProvider. As for test coverage, I opted to find parity in the coverage for the GEI tests that is relevant to bbs2gh. I'm happy to add more coverage if desired 👍 Tasks Did you write/update appropriate tests Release notes updated (if appropriate) Appropriate logging output Issue linked Docs updated (or issue created) (added bullet point to #586)
1 parent bde2950 commit 983803d

File tree

8 files changed

+371
-75
lines changed

8 files changed

+371
-75
lines changed

RELEASENOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
- Add `migrate-repo` command to `bbs2gh`.

src/Octoshift/GithubApi.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,32 @@ public virtual async Task<string> CreateAdoMigrationSource(string orgId, string
171171
return (string)data["data"]["createMigrationSource"]["migrationSource"]["id"];
172172
}
173173

174+
public virtual async Task<string> CreateBbsMigrationSource(string orgId)
175+
{
176+
var url = $"{_apiUrl}/graphql";
177+
178+
var query = "mutation createMigrationSource($name: String!, $url: String!, $ownerId: ID!, $type: MigrationSourceType!)";
179+
var gql = "createMigrationSource(input: {name: $name, url: $url, ownerId: $ownerId, type: $type}) { migrationSource { id, name, url, type } }";
180+
181+
var payload = new
182+
{
183+
query = $"{query} {{ {gql} }}",
184+
variables = new
185+
{
186+
name = "Bitbucket Server Source",
187+
url = "https://not-used",
188+
ownerId = orgId,
189+
type = "BITBUCKET_SERVER"
190+
},
191+
operationName = "createMigrationSource"
192+
};
193+
194+
var response = await _client.PostAsync(url, payload);
195+
var data = JObject.Parse(response);
196+
197+
return (string)data["data"]["createMigrationSource"]["migrationSource"]["id"];
198+
}
199+
174200
public virtual async Task<string> CreateGhecMigrationSource(string orgId)
175201
{
176202
var url = $"{_apiUrl}/graphql";
@@ -268,6 +294,20 @@ mutation startRepositoryMigration(
268294
return (string)data["data"]["startRepositoryMigration"]["repositoryMigration"]["id"];
269295
}
270296

297+
public virtual async Task<string> StartBbsMigration(string migrationSourceId, string orgId, string repo, string targetToken, string archiveUrl)
298+
{
299+
return await StartMigration(
300+
migrationSourceId,
301+
"https://not-used", // source repository URL
302+
orgId,
303+
repo,
304+
"not-used", // source access token
305+
targetToken,
306+
archiveUrl,
307+
"https://not-used" // metadata archive URL
308+
);
309+
}
310+
271311
public virtual async Task<(string State, string RepositoryName, string FailureReason)> GetMigration(string migrationId)
272312
{
273313
var url = $"{_apiUrl}/graphql";

src/OctoshiftCLI.Tests/GithubApiTests.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,42 @@ public async Task CreateAdoMigrationSource_Uses_Ado_Server_Url()
472472
expectedMigrationSourceId.Should().Be(actualMigrationSourceId);
473473
}
474474

475+
[Fact]
476+
public async Task CreateBbsMigrationSource_Returns_New_Migration_Source_Id()
477+
{
478+
// Arrange
479+
const string url = "https://api.github.com/graphql";
480+
const string orgId = "ORG_ID";
481+
var payload =
482+
"{\"query\":\"mutation createMigrationSource($name: String!, $url: String!, $ownerId: ID!, $type: MigrationSourceType!) " +
483+
"{ createMigrationSource(input: {name: $name, url: $url, ownerId: $ownerId, type: $type}) { migrationSource { id, name, url, type } } }\"" +
484+
$",\"variables\":{{\"name\":\"Bitbucket Server Source\",\"url\":\"https://not-used\",\"ownerId\":\"{orgId}\",\"type\":\"BITBUCKET_SERVER\"}},\"operationName\":\"createMigrationSource\"}}";
485+
const string actualMigrationSourceId = "MS_kgC4NjFhOTVjOTc4ZTRhZjEwMDA5NjNhOTdm";
486+
var response = $@"
487+
{{
488+
""data"": {{
489+
""createMigrationSource"": {{
490+
""migrationSource"": {{
491+
""id"": ""{actualMigrationSourceId}"",
492+
""name"": ""Bitbucket Server Source"",
493+
""url"": ""https://not-used"",
494+
""type"": ""BITBUCKET_SERVER""
495+
}}
496+
}}
497+
}}
498+
}}";
499+
500+
_githubClientMock
501+
.Setup(m => m.PostAsync(url, It.Is<object>(x => x.ToJson() == payload)))
502+
.ReturnsAsync(response);
503+
504+
// Act
505+
var expectedMigrationSourceId = await _githubApi.CreateBbsMigrationSource(orgId);
506+
507+
// Assert
508+
expectedMigrationSourceId.Should().Be(actualMigrationSourceId);
509+
}
510+
475511
[Fact]
476512
public async Task CreateGhecMigrationSource_Returns_New_Migration_Source_Id()
477513
{
@@ -609,6 +645,108 @@ mutation startRepositoryMigration(
609645
expectedRepositoryMigrationId.Should().Be(actualRepositoryMigrationId);
610646
}
611647

648+
[Fact]
649+
public async Task StartBbsMigration_Returns_New_Repository_Migration_Id()
650+
{
651+
// Arrange
652+
const string migrationSourceId = "MIGRATION_SOURCE_ID";
653+
const string orgId = "ORG_ID";
654+
const string url = "https://api.github.com/graphql";
655+
const string gitArchiveUrl = "GIT_ARCHIVE_URL";
656+
const string targetToken = "TARGET_TOKEN";
657+
658+
const string unusedSourceRepoUrl = "https://not-used";
659+
const string unusedSourceToken = "not-used";
660+
const string unusedMetadataArchiveUrl = "https://not-used";
661+
662+
const string query = @"
663+
mutation startRepositoryMigration(
664+
$sourceId: ID!,
665+
$ownerId: ID!,
666+
$sourceRepositoryUrl: URI!,
667+
$repositoryName: String!,
668+
$continueOnError: Boolean!,
669+
$gitArchiveUrl: String,
670+
$metadataArchiveUrl: String,
671+
$accessToken: String!,
672+
$githubPat: String,
673+
$skipReleases: Boolean)";
674+
const string gql = @"
675+
startRepositoryMigration(
676+
input: {
677+
sourceId: $sourceId,
678+
ownerId: $ownerId,
679+
sourceRepositoryUrl: $sourceRepositoryUrl,
680+
repositoryName: $repositoryName,
681+
continueOnError: $continueOnError,
682+
gitArchiveUrl: $gitArchiveUrl,
683+
metadataArchiveUrl: $metadataArchiveUrl,
684+
accessToken: $accessToken,
685+
githubPat: $githubPat,
686+
skipReleases: $skipReleases
687+
}
688+
) {
689+
repositoryMigration {
690+
id,
691+
migrationSource {
692+
id,
693+
name,
694+
type
695+
},
696+
sourceUrl,
697+
state,
698+
failureReason
699+
}
700+
}";
701+
var payload = new
702+
{
703+
query = $"{query} {{ {gql} }}",
704+
variables = new
705+
{
706+
sourceId = migrationSourceId,
707+
ownerId = orgId,
708+
sourceRepositoryUrl = unusedSourceRepoUrl,
709+
repositoryName = GITHUB_REPO,
710+
continueOnError = true,
711+
gitArchiveUrl,
712+
metadataArchiveUrl = unusedMetadataArchiveUrl,
713+
accessToken = unusedSourceToken,
714+
githubPat = targetToken,
715+
skipReleases = false
716+
},
717+
operationName = "startRepositoryMigration"
718+
};
719+
const string actualRepositoryMigrationId = "RM_kgC4NjFhNmE2NGU2ZWE1YTQwMDA5ODliZjhi";
720+
var response = $@"
721+
{{
722+
""data"": {{
723+
""startRepositoryMigration"": {{
724+
""repositoryMigration"": {{
725+
""id"": ""{actualRepositoryMigrationId}"",
726+
""migrationSource"": {{
727+
""id"": ""MS_kgC4NjFhNmE2NDViNWZmOTEwMDA5MTZiMGQw"",
728+
""name"": ""Azure Devops Source"",
729+
""type"": ""AZURE_DEVOPS""
730+
}},
731+
""sourceUrl"": ""https://dev.azure.com/github-inside-msft/Team-Demos/_git/Tiny"",
732+
""state"": ""QUEUED"",
733+
""failureReason"": """"
734+
}}
735+
}}
736+
}}
737+
}}";
738+
739+
_githubClientMock
740+
.Setup(m => m.PostAsync(url, It.Is<object>(x => x.ToJson() == payload.ToJson())))
741+
.ReturnsAsync(response);
742+
743+
// Act
744+
var expectedRepositoryMigrationId = await _githubApi.StartBbsMigration(migrationSourceId, orgId, GITHUB_REPO, targetToken, gitArchiveUrl);
745+
746+
// Assert
747+
expectedRepositoryMigrationId.Should().Be(actualRepositoryMigrationId);
748+
}
749+
612750
[Fact]
613751
public async Task StartMigration_Throws_When_GraphQL_Response_Has_Errors()
614752
{

src/OctoshiftCLI.Tests/OctoshiftCLI.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<ItemGroup>
1212
<ProjectReference Include="..\ado2gh\ado2gh.csproj" />
13+
<ProjectReference Include="..\bbs2gh\bbs2gh.csproj" />
1314
<ProjectReference Include="..\gei\gei.csproj" />
1415
<PackageReference Include="FluentAssertions" Version="6.2.0" />
1516
<PackageReference Include="JunitXml.TestLogger" Version="3.0.98" />
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
using System.Threading.Tasks;
2+
using FluentAssertions;
3+
using Moq;
4+
using OctoshiftCLI.BbsToGithub;
5+
using OctoshiftCLI.BbsToGithub.Commands;
6+
using Xunit;
7+
8+
namespace OctoshiftCLI.Tests.BbsToGithub.Commands
9+
{
10+
public class MigrateRepoCommandTests
11+
{
12+
private readonly Mock<GithubApi> _mockGithubApi = TestHelpers.CreateMock<GithubApi>();
13+
private readonly Mock<GithubApiFactory> _mockGithubApiFactory = TestHelpers.CreateMock<GithubApiFactory>();
14+
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
15+
private readonly Mock<EnvironmentVariableProvider> _mockEnvironmentVariableProvider = TestHelpers.CreateMock<EnvironmentVariableProvider>();
16+
17+
private readonly MigrateRepoCommand _command;
18+
19+
private const string ARCHIVE_URL = "https://archive-url/bbs-archive.tar";
20+
private const string GITHUB_ORG = "target-org";
21+
private const string GITHUB_REPO = "target-repo";
22+
private const string GITHUB_PAT = "github pat";
23+
24+
private const string GITHUB_ORG_ID = "github-org-id";
25+
private const string MIGRATION_SOURCE_ID = "migration-source-id";
26+
private const string MIGRATION_ID = "migration-id";
27+
28+
public MigrateRepoCommandTests()
29+
{
30+
_command = new MigrateRepoCommand(
31+
_mockOctoLogger.Object,
32+
_mockGithubApiFactory.Object,
33+
_mockEnvironmentVariableProvider.Object
34+
);
35+
}
36+
37+
[Fact]
38+
public void Should_Have_Options()
39+
{
40+
_command.Should().NotBeNull();
41+
_command.Name.Should().Be("migrate-repo");
42+
_command.Options.Count.Should().Be(6);
43+
44+
TestHelpers.VerifyCommandOption(_command.Options, "archive-url", true);
45+
TestHelpers.VerifyCommandOption(_command.Options, "github-org", true);
46+
TestHelpers.VerifyCommandOption(_command.Options, "github-repo", true);
47+
TestHelpers.VerifyCommandOption(_command.Options, "github-pat", false);
48+
TestHelpers.VerifyCommandOption(_command.Options, "wait", false);
49+
TestHelpers.VerifyCommandOption(_command.Options, "verbose", false);
50+
}
51+
52+
[Fact]
53+
public async Task Happy_Path()
54+
{
55+
// Arrange
56+
_mockEnvironmentVariableProvider.Setup(m => m.GithubPersonalAccessToken()).Returns(GITHUB_PAT);
57+
_mockGithubApiFactory.Setup(m => m.Create(It.IsAny<string>(), It.IsAny<string>())).Returns(_mockGithubApi.Object);
58+
59+
_mockGithubApi.Setup(x => x.GetOrganizationId(GITHUB_ORG).Result).Returns(GITHUB_ORG_ID);
60+
_mockGithubApi.Setup(x => x.CreateBbsMigrationSource(GITHUB_ORG_ID).Result).Returns(MIGRATION_SOURCE_ID);
61+
_mockGithubApi
62+
.Setup(x => x.StartBbsMigration(MIGRATION_SOURCE_ID, GITHUB_ORG_ID, GITHUB_REPO, GITHUB_PAT, ARCHIVE_URL).Result)
63+
.Returns(MIGRATION_ID);
64+
65+
// Act
66+
var args = new MigrateRepoCommandArgs
67+
{
68+
ArchiveUrl = ARCHIVE_URL,
69+
GithubOrg = GITHUB_ORG,
70+
GithubRepo = GITHUB_REPO
71+
};
72+
await _command.Invoke(args);
73+
74+
// Assert
75+
_mockGithubApi.Verify(m => m.StartBbsMigration(
76+
MIGRATION_SOURCE_ID,
77+
GITHUB_ORG_ID,
78+
GITHUB_REPO,
79+
GITHUB_PAT,
80+
ARCHIVE_URL
81+
));
82+
}
83+
84+
[Fact]
85+
public async Task Uses_GitHub_Pat_When_Provided_As_Option()
86+
{
87+
// Arrange
88+
var githubPat = "specific github pat";
89+
90+
_mockGithubApiFactory.Setup(m => m.Create(It.IsAny<string>(), githubPat)).Returns(_mockGithubApi.Object);
91+
92+
_mockGithubApi.Setup(x => x.GetOrganizationId(GITHUB_ORG).Result).Returns(GITHUB_ORG_ID);
93+
_mockGithubApi.Setup(x => x.CreateBbsMigrationSource(GITHUB_ORG_ID).Result).Returns(MIGRATION_SOURCE_ID);
94+
_mockGithubApi
95+
.Setup(x => x.StartBbsMigration(MIGRATION_SOURCE_ID, GITHUB_ORG_ID, GITHUB_REPO, GITHUB_PAT, ARCHIVE_URL).Result)
96+
.Returns(MIGRATION_ID);
97+
98+
// Act
99+
var args = new MigrateRepoCommandArgs
100+
{
101+
ArchiveUrl = ARCHIVE_URL,
102+
GithubOrg = GITHUB_ORG,
103+
GithubRepo = GITHUB_REPO,
104+
GithubPat = githubPat
105+
};
106+
await _command.Invoke(args);
107+
108+
// Assert
109+
_mockGithubApi.Verify(m => m.StartBbsMigration(
110+
MIGRATION_SOURCE_ID,
111+
GITHUB_ORG_ID,
112+
GITHUB_REPO,
113+
githubPat,
114+
ARCHIVE_URL
115+
));
116+
}
117+
118+
[Fact]
119+
public async Task Skip_Migration_If_Target_Repo_Exists()
120+
{
121+
// Arrange
122+
_mockEnvironmentVariableProvider.Setup(m => m.GithubPersonalAccessToken()).Returns(GITHUB_PAT);
123+
_mockGithubApiFactory.Setup(m => m.Create(It.IsAny<string>(), It.IsAny<string>())).Returns(_mockGithubApi.Object);
124+
125+
_mockGithubApi.Setup(x => x.GetOrganizationId(GITHUB_ORG).Result).Returns(GITHUB_ORG_ID);
126+
_mockGithubApi.Setup(x => x.CreateBbsMigrationSource(GITHUB_ORG_ID).Result).Returns(MIGRATION_SOURCE_ID);
127+
_mockGithubApi
128+
.Setup(x => x.StartBbsMigration(MIGRATION_SOURCE_ID, GITHUB_ORG_ID, GITHUB_REPO, GITHUB_PAT, ARCHIVE_URL).Result)
129+
.Throws(new OctoshiftCliException($"A repository called {GITHUB_ORG}/{GITHUB_REPO} already exists"));
130+
131+
// Act
132+
var args = new MigrateRepoCommandArgs
133+
{
134+
ArchiveUrl = ARCHIVE_URL,
135+
GithubOrg = GITHUB_ORG,
136+
GithubRepo = GITHUB_REPO
137+
};
138+
await _command.Invoke(args);
139+
140+
// Assert
141+
_mockOctoLogger.Verify(m => m.LogWarning(It.IsAny<string>()), Times.Exactly(1));
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)