Skip to content

Commit 02e80d8

Browse files
authored
Merge branch 'main' into lindluni/code-scanning-exclusion
2 parents be2a4b9 + 27cf3c3 commit 02e80d8

14 files changed

Lines changed: 193 additions & 47 deletions

File tree

src/Octoshift/Factories/GithubApiFactory.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ GithubApi ISourceGithubApiFactory.Create(string apiUrl, string uploadsUrl, strin
3131
apiUrl ??= DEFAULT_API_URL;
3232
uploadsUrl ??= DEFAULT_UPLOADS_URL;
3333
sourcePersonalAccessToken ??= _environmentVariableProvider.SourceGithubPersonalAccessToken();
34-
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("Default"), _versionProvider, _retryPolicy, _dateTimeProvider, sourcePersonalAccessToken);
34+
var clientRetryPolicy = (_retryPolicy ?? new RetryPolicy(_octoLogger)).WithServiceName("GitHub");
35+
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("Default"), _versionProvider, clientRetryPolicy, _dateTimeProvider, sourcePersonalAccessToken);
3536
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy, _environmentVariableProvider);
3637
return new GithubApi(githubClient, apiUrl, _retryPolicy, multipartUploader);
3738
}
@@ -41,7 +42,8 @@ GithubApi ISourceGithubApiFactory.CreateClientNoSsl(string apiUrl, string upload
4142
apiUrl ??= DEFAULT_API_URL;
4243
uploadsUrl ??= DEFAULT_UPLOADS_URL;
4344
sourcePersonalAccessToken ??= _environmentVariableProvider.SourceGithubPersonalAccessToken();
44-
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("NoSSL"), _versionProvider, _retryPolicy, _dateTimeProvider, sourcePersonalAccessToken);
45+
var clientRetryPolicy = (_retryPolicy ?? new RetryPolicy(_octoLogger)).WithServiceName("GitHub");
46+
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("NoSSL"), _versionProvider, clientRetryPolicy, _dateTimeProvider, sourcePersonalAccessToken);
4547
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy, _environmentVariableProvider);
4648
return new GithubApi(githubClient, apiUrl, _retryPolicy, multipartUploader);
4749
}
@@ -51,7 +53,8 @@ GithubApi ITargetGithubApiFactory.Create(string apiUrl, string uploadsUrl, strin
5153
apiUrl ??= DEFAULT_API_URL;
5254
uploadsUrl ??= DEFAULT_UPLOADS_URL;
5355
targetPersonalAccessToken ??= _environmentVariableProvider.TargetGithubPersonalAccessToken();
54-
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("Default"), _versionProvider, _retryPolicy, _dateTimeProvider, targetPersonalAccessToken);
56+
var clientRetryPolicy = (_retryPolicy ?? new RetryPolicy(_octoLogger)).WithServiceName("GitHub");
57+
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("Default"), _versionProvider, clientRetryPolicy, _dateTimeProvider, targetPersonalAccessToken);
5558
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy, _environmentVariableProvider);
5659
return new GithubApi(githubClient, apiUrl, _retryPolicy, multipartUploader);
5760
}

src/Octoshift/RetryPolicy.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,26 @@ namespace OctoshiftCLI
1010
public class RetryPolicy
1111
{
1212
private readonly OctoLogger _log;
13+
private readonly string _serviceName;
1314
internal int _httpRetryInterval = 1000;
1415
internal int _retryInterval = 4000;
1516

16-
public RetryPolicy(OctoLogger log)
17+
public RetryPolicy(OctoLogger log, string serviceName = null)
1718
{
1819
_log = log;
20+
_serviceName = serviceName;
1921
}
2022

23+
/// <summary>
24+
/// Returns a new RetryPolicy with the same configuration as this one, but tagged with a service name
25+
/// for more actionable error messages (e.g. on 401 Unauthorized).
26+
/// </summary>
27+
public RetryPolicy WithServiceName(string serviceName) => new(_log, serviceName)
28+
{
29+
_httpRetryInterval = _httpRetryInterval,
30+
_retryInterval = _retryInterval
31+
};
32+
2133
public async Task<T> HttpRetry<T>(Func<Task<T>> func, Func<HttpRequestException, bool> filter)
2234
{
2335
var policy = Policy.Handle(filter)
@@ -26,7 +38,15 @@ public async Task<T> HttpRetry<T>(Func<Task<T>> func, Func<HttpRequestException,
2638
_log?.LogVerbose($"Call failed with HTTP {((HttpRequestException)ex).StatusCode}, retrying...");
2739
});
2840

29-
return await policy.ExecuteAsync(func);
41+
try
42+
{
43+
return await policy.ExecuteAsync(func);
44+
}
45+
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
46+
{
47+
ThrowUnauthorizedException(ex);
48+
throw; // unreachable, but required by compiler
49+
}
3050
}
3151

3252
public async Task<PolicyResult<T>> RetryOnResult<T>(Func<Task<T>> func, Func<T, bool> resultPredicate, string retryLogMessage = null)
@@ -53,15 +73,20 @@ private AsyncRetryPolicy CreateRetryPolicyForException<TException>() where TExce
5373
_log?.LogVerbose($"[HTTP ERROR {(int?)httpEx.StatusCode}] {ex}");
5474
if (httpEx.StatusCode == System.Net.HttpStatusCode.Unauthorized)
5575
{
56-
// We should not retry on an unathorized error; instead, log and bubble up the error
57-
throw new OctoshiftCliException($"Unauthorized. Please check your token and try again", ex);
58-
};
76+
ThrowUnauthorizedException(httpEx);
77+
}
5978
}
6079
else
6180
{
6281
_log?.LogVerbose(ex.ToString());
6382
}
6483
_log?.LogVerbose("Retrying...");
6584
});
85+
86+
private void ThrowUnauthorizedException(HttpRequestException ex)
87+
{
88+
var serviceContext = _serviceName is not null ? $" ({_serviceName})" : "";
89+
throw new OctoshiftCliException($"Unauthorized{serviceContext}. Please check your token and try again", ex);
90+
}
6691
}
6792
}

src/Octoshift/Services/GithubApi.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -561,14 +561,14 @@ ... on Migration {
561561
{
562562
return await _retryPolicy.Retry(async () =>
563563
{
564-
var data = await _client.PostGraphQLAsync(url, payload);
564+
var data = await _client.PostGraphQLWithRetryAsync(url, payload);
565565

566566
return (
567-
State: (string)data["data"]["node"]["state"],
568-
RepositoryName: (string)data["data"]["node"]["repositoryName"],
569-
WarningsCount: (int)data["data"]["node"]["warningsCount"],
570-
FailureReason: (string)data["data"]["node"]["failureReason"],
571-
MigrationLogUrl: (string)data["data"]["node"]["migrationLogUrl"]);
567+
State: (string)data["data"]["node"]["state"],
568+
RepositoryName: (string)data["data"]["node"]["repositoryName"],
569+
WarningsCount: (int)data["data"]["node"]["warningsCount"],
570+
FailureReason: (string)data["data"]["node"]["failureReason"],
571+
MigrationLogUrl: (string)data["data"]["node"]["migrationLogUrl"]);
572572
});
573573
}
574574
catch (Exception ex)

src/Octoshift/Services/GithubClient.cs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ public class GithubClient
2525
private const int SECONDARY_RATE_LIMIT_MAX_RETRIES = 3;
2626
private const int SECONDARY_RATE_LIMIT_DEFAULT_DELAY = 60; // 60 seconds default delay
2727

28+
internal int _secondaryRateLimitMaxRetries = SECONDARY_RATE_LIMIT_MAX_RETRIES; // Exposed for testing purposes
29+
internal int _secondaryRateLimitDefaultDelay = SECONDARY_RATE_LIMIT_DEFAULT_DELAY; // Exposed for testing purposes
30+
2831
public GithubClient(OctoLogger log, HttpClient httpClient, IVersionProvider versionProvider, RetryPolicy retryPolicy, DateTimeProvider dateTimeProvider, string personalAccessToken)
2932
{
3033
_log = log;
@@ -93,6 +96,36 @@ public virtual async Task<JToken> PostGraphQLAsync(
9396
return data;
9497
}
9598

99+
public virtual async Task<JToken> PostGraphQLWithRetryAsync(
100+
string url,
101+
object body,
102+
Dictionary<string, string> customHeaders = null,
103+
int retryCount = 0)
104+
{
105+
var currentRetryCount = retryCount;
106+
107+
while (true)
108+
{
109+
var (response, headers) = await PostWithRetry(url, body, customHeaders);
110+
111+
if (IsGraphQLSecondaryRateLimit(response))
112+
{
113+
(response, _) = await HandleSecondaryRateLimit(HttpMethod.Post, url, body, HttpStatusCode.OK, customHeaders, headers, currentRetryCount);
114+
115+
if (IsGraphQLSecondaryRateLimit(response))
116+
{
117+
currentRetryCount++;
118+
continue;
119+
}
120+
}
121+
122+
var data = JObject.Parse(response);
123+
EnsureSuccessGraphQLResponse(data);
124+
125+
return data;
126+
}
127+
}
128+
96129
public virtual async IAsyncEnumerable<JToken> PostGraphQLWithPaginationAsync(
97130
string url,
98131
object body,
@@ -156,6 +189,13 @@ public virtual async Task<string> PatchAsync(string url, object body, Dictionary
156189
HttpStatusCode expectedStatus = HttpStatusCode.OK) =>
157190
await _retryPolicy.Retry(async () => await SendAsync(HttpMethod.Get, url, customHeaders: customHeaders, expectedStatus: expectedStatus));
158191

192+
private async Task<(string Content, KeyValuePair<string, IEnumerable<string>>[] ResponseHeaders)> PostWithRetry(
193+
string url,
194+
object body,
195+
Dictionary<string, string> customHeaders = null,
196+
HttpStatusCode expectedStatus = HttpStatusCode.OK) =>
197+
await _retryPolicy.Retry(async () => await SendAsync(HttpMethod.Post, url, body, customHeaders: customHeaders, expectedStatus: expectedStatus));
198+
159199
private async Task<(string Content, KeyValuePair<string, IEnumerable<string>>[] ResponseHeaders)> SendAsync(
160200
HttpMethod httpMethod,
161201
string url,
@@ -312,6 +352,22 @@ private bool IsSecondaryRateLimit(HttpStatusCode statusCode, string content)
312352
statusCode == HttpStatusCode.TooManyRequests;
313353
}
314354

355+
private bool IsGraphQLSecondaryRateLimit(string content)
356+
{
357+
var response = JObject.Parse(content);
358+
359+
if (response.TryGetValue("errors", out var jErrors) && jErrors is JArray { Count: > 0 } errors)
360+
{
361+
var error = (JObject)errors[0];
362+
if (error.TryGetValue("type", out var jType) && jType.Type == JTokenType.String)
363+
{
364+
return ((string)jType) == "RATE_LIMIT";
365+
}
366+
}
367+
368+
return false;
369+
}
370+
315371
private async Task<(string Content, KeyValuePair<string, IEnumerable<string>>[] ResponseHeaders)> HandleSecondaryRateLimit(
316372
HttpMethod httpMethod,
317373
string url,
@@ -321,7 +377,7 @@ private bool IsSecondaryRateLimit(HttpStatusCode statusCode, string content)
321377
KeyValuePair<string, IEnumerable<string>>[] headers,
322378
int retryCount = 0)
323379
{
324-
if (retryCount >= SECONDARY_RATE_LIMIT_MAX_RETRIES)
380+
if (retryCount >= _secondaryRateLimitMaxRetries)
325381
{
326382
throw new OctoshiftCliException($"Secondary rate limit exceeded. Maximum retries ({SECONDARY_RATE_LIMIT_MAX_RETRIES}) reached. Please wait before retrying your request.");
327383
}
@@ -359,6 +415,6 @@ private int GetSecondaryRateLimitDelay(KeyValuePair<string, IEnumerable<string>>
359415
}
360416

361417
// Otherwise use exponential backoff: 1m → 2m → 4m
362-
return SECONDARY_RATE_LIMIT_DEFAULT_DELAY * (int)Math.Pow(2, retryCount);
418+
return _secondaryRateLimitDefaultDelay * (int)Math.Pow(2, retryCount);
363419
}
364420
}

src/OctoshiftCLI.IntegrationTests/AdoToGithub.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,23 @@ protected AdoToGithub(ITestOutputHelper output, string adoServerUrl = "https://d
2323
StartTime = DateTime.Now;
2424
_output = output;
2525

26+
TestHelper.AssertCredentialsPresent(
27+
(adoPatEnvVar, "Azure DevOps personal access token"),
28+
("GHEC_PAT", "GitHub Enterprise Cloud personal access token"));
29+
2630
var logger = new OctoLogger(x => { }, x => _output.WriteLine(x), x => { }, x => { });
2731

2832
_versionClient = new HttpClient();
2933
var adoToken = Environment.GetEnvironmentVariable(adoPatEnvVar);
3034
_adoHttpClient = new HttpClient();
31-
var retryPolicy = new RetryPolicy(logger);
35+
var retryPolicy = new RetryPolicy(logger, $"Azure DevOps ({adoPatEnvVar})");
3236
var adoClient = new AdoClient(logger, _adoHttpClient, new VersionChecker(_versionClient, logger), retryPolicy, adoToken);
3337
var adoApi = new AdoApi(adoClient, adoServerUrl, logger);
3438

3539
var githubToken = Environment.GetEnvironmentVariable("GHEC_PAT");
3640
_githubHttpClient = new HttpClient();
37-
var githubClient = new GithubClient(logger, _githubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger), new DateTimeProvider(), githubToken);
38-
var githubApi = new GithubApi(githubClient, "https://api.github.com", new RetryPolicy(logger), null);
41+
var githubClient = new GithubClient(logger, _githubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger, "GitHub (GHEC_PAT)"), new DateTimeProvider(), githubToken);
42+
var githubApi = new GithubApi(githubClient, "https://api.github.com", new RetryPolicy(logger, "GitHub (GHEC_PAT)"), null);
3943

4044
Tokens = new Dictionary<string, string>
4145
{

src/OctoshiftCLI.IntegrationTests/BbsToGithub.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,19 @@ public BbsToGithub(ITestOutputHelper output)
3939
_startTime = DateTime.Now;
4040
_output = output;
4141

42+
var azureStorageEnvVar = $"AZURE_STORAGE_CONNECTION_STRING_BBS_{TestHelper.GetOsName().ToUpperInvariant()}";
43+
TestHelper.AssertCredentialsPresent(
44+
("BBS_USERNAME", "Bitbucket Server username"),
45+
("BBS_PASSWORD", "Bitbucket Server password"),
46+
("GHEC_PAT", "GitHub Enterprise Cloud personal access token"),
47+
(azureStorageEnvVar, "Azure blob storage connection string for BBS migration archives"));
48+
4249
_logger = new OctoLogger(_ => { }, x => _output.WriteLine(x), _ => { }, _ => { });
4350

4451
var sourceBbsUsername = Environment.GetEnvironmentVariable("BBS_USERNAME");
4552
var sourceBbsPassword = Environment.GetEnvironmentVariable("BBS_PASSWORD");
4653
var targetGithubToken = Environment.GetEnvironmentVariable("GHEC_PAT");
47-
_azureStorageConnectionString = Environment.GetEnvironmentVariable($"AZURE_STORAGE_CONNECTION_STRING_BBS_{TestHelper.GetOsName().ToUpper()}");
54+
_azureStorageConnectionString = Environment.GetEnvironmentVariable(azureStorageEnvVar);
4855
_tokens = new Dictionary<string, string>
4956
{
5057
["BBS_USERNAME"] = sourceBbsUsername,
@@ -55,14 +62,14 @@ public BbsToGithub(ITestOutputHelper output)
5562
_versionClient = new HttpClient();
5663

5764
_sourceBbsHttpClient = new HttpClient();
58-
_sourceBbsClient = new BbsClient(_logger, _sourceBbsHttpClient, new VersionChecker(_versionClient, _logger), new RetryPolicy(_logger), sourceBbsUsername, sourceBbsPassword);
65+
_sourceBbsClient = new BbsClient(_logger, _sourceBbsHttpClient, new VersionChecker(_versionClient, _logger), new RetryPolicy(_logger, "Bitbucket Server (BBS_USERNAME/BBS_PASSWORD)"), sourceBbsUsername, sourceBbsPassword);
5966

6067
_targetGithubHttpClient = new HttpClient();
61-
_targetGithubClient = new GithubClient(_logger, _targetGithubHttpClient, new VersionChecker(_versionClient, _logger), new RetryPolicy(_logger), new DateTimeProvider(), targetGithubToken);
62-
var retryPolicy = new RetryPolicy(_logger);
68+
_targetGithubClient = new GithubClient(_logger, _targetGithubHttpClient, new VersionChecker(_versionClient, _logger), new RetryPolicy(_logger, "GitHub (GHEC_PAT)"), new DateTimeProvider(), targetGithubToken);
69+
var retryPolicy = new RetryPolicy(_logger, "GitHub (GHEC_PAT)");
6370
var environmentVariableProvider = new EnvironmentVariableProvider(_logger);
6471
_archiveUploader = new ArchiveUploader(_targetGithubClient, UPLOADS_URL, _logger, retryPolicy, environmentVariableProvider);
65-
_targetGithubApi = new GithubApi(_targetGithubClient, "https://api.github.com", new RetryPolicy(_logger), _archiveUploader);
72+
_targetGithubApi = new GithubApi(_targetGithubClient, "https://api.github.com", new RetryPolicy(_logger, "GitHub (GHEC_PAT)"), _archiveUploader);
6673

6774
_blobServiceClient = new BlobServiceClient(_azureStorageConnectionString);
6875

src/OctoshiftCLI.IntegrationTests/GhesToGithub.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,35 @@ public GhesToGithub(ITestOutputHelper output)
3636
_startTime = DateTime.Now;
3737
_output = output;
3838

39+
var azureStorageEnvVar = $"AZURE_STORAGE_CONNECTION_STRING_GHES_{TestHelper.GetOsName().ToUpperInvariant()}";
40+
TestHelper.AssertCredentialsPresent(
41+
("GHES_PAT", "GitHub Enterprise Server personal access token"),
42+
("GHEC_PAT", "GitHub Enterprise Cloud personal access token"),
43+
(azureStorageEnvVar, "Azure blob storage connection string for GHES migration archives"));
44+
3945
var logger = new OctoLogger(_ => { }, x => _output.WriteLine(x), _ => { }, _ => { });
4046

4147
var sourceGithubToken = Environment.GetEnvironmentVariable("GHES_PAT");
4248
var targetGithubToken = Environment.GetEnvironmentVariable("GHEC_PAT");
43-
_azureStorageConnectionString = Environment.GetEnvironmentVariable($"AZURE_STORAGE_CONNECTION_STRING_GHES_{TestHelper.GetOsName().ToUpper()}");
49+
_azureStorageConnectionString = Environment.GetEnvironmentVariable(azureStorageEnvVar);
4450
_tokens = new Dictionary<string, string>
4551
{
4652
["GH_SOURCE_PAT"] = sourceGithubToken,
4753
["GH_PAT"] = targetGithubToken,
4854
};
4955

5056
_versionClient = new HttpClient();
51-
var retryPolicy = new RetryPolicy(logger);
57+
var retryPolicy = new RetryPolicy(logger, "GHES (GHES_PAT)");
5258
var environmentVariableProvider = new EnvironmentVariableProvider(logger);
5359

5460
_sourceGithubHttpClient = new HttpClient();
55-
_sourceGithubClient = new GithubClient(logger, _sourceGithubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger), new DateTimeProvider(), sourceGithubToken);
61+
_sourceGithubClient = new GithubClient(logger, _sourceGithubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger, "GHES (GHES_PAT)"), new DateTimeProvider(), sourceGithubToken);
5662
_archiveUploader = new ArchiveUploader(_targetGithubClient, UPLOADS_URL, logger, retryPolicy, environmentVariableProvider);
57-
_sourceGithubApi = new GithubApi(_sourceGithubClient, GHES_API_URL, new RetryPolicy(logger), _archiveUploader);
63+
_sourceGithubApi = new GithubApi(_sourceGithubClient, GHES_API_URL, new RetryPolicy(logger, "GHES (GHES_PAT)"), _archiveUploader);
5864

5965
_targetGithubHttpClient = new HttpClient();
60-
_targetGithubClient = new GithubClient(logger, _targetGithubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger), new DateTimeProvider(), targetGithubToken);
61-
_targetGithubApi = new GithubApi(_targetGithubClient, "https://api.github.com", new RetryPolicy(logger), _archiveUploader);
66+
_targetGithubClient = new GithubClient(logger, _targetGithubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger, "GitHub (GHEC_PAT)"), new DateTimeProvider(), targetGithubToken);
67+
_targetGithubApi = new GithubApi(_targetGithubClient, "https://api.github.com", new RetryPolicy(logger, "GitHub (GHEC_PAT)"), _archiveUploader);
6268

6369
_blobServiceClient = new BlobServiceClient(_azureStorageConnectionString);
6470

0 commit comments

Comments
 (0)