Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9a240b6
Add support for overriding multipart upload chunk size via environmen…
tspascoal Jul 30, 2025
234e769
Improve release note
tspascoal Jul 30, 2025
fd49f02
Fix warnings
tspascoal Jul 30, 2025
fa462b5
Isolate archive uploader tests
tspascoal Jul 30, 2025
7a93be3
Refactor ArchiveUploader to use EnvironmentVariableProvider
tspascoal Jul 30, 2025
688d690
Fix code scanning warning
tspascoal Jul 30, 2025
eeec27f
Merge branch 'main' into override-multipart-size
tspascoal Dec 6, 2025
678d234
Fix formatting in ArchiveUploader initialization
tspascoal Dec 6, 2025
b9b5028
Apply suggestions from code review
tspascoal Dec 6, 2025
a4e6f5a
Update log messages in ArchiveUploaderTests to reflect multipart uplo…
tspascoal Dec 6, 2025
1f8ed87
Merge branch 'override-multipart-size' of https://github.com/tspascoa…
tspascoal Dec 6, 2025
daa636d
Update log messages in ArchiveUploader to use human-readable size format
tspascoal Dec 6, 2025
2eb6c0a
Change the value of the multipart upload storage override to medibyte…
tspascoal Dec 7, 2025
387ceef
Change the value of the multipart upload storage override to medibyte…
tspascoal Dec 7, 2025
44567e9
Merge branch 'override-multipart-size' of https://github.com/tspascoa…
tspascoal Dec 7, 2025
17fe730
Merge branch 'main' into brianaj/external-pr-1403
brianaj Dec 8, 2025
ef6779a
Update command descriptions to also mention GITHUB_OWNED_STORAGE_MULT…
brianaj Dec 8, 2025
be2c1cc
address first round of pr review comments
brianaj Dec 8, 2025
87bd5ec
use logMock.Object instead of class field
brianaj Dec 8, 2025
797de18
fix indentation issues
brianaj Dec 8, 2025
1073275
Merge branch 'main' into brianaj/external-pr-1403
brianaj Dec 8, 2025
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ When the CLI is launched, it logs if a newer version of the CLI is available. Yo

When the CLI is launched, it logs a warning if there are any ongoing [GitHub incidents](https://www.githubstatus.com/) that might affect your use of the CLI. You can skip this check by setting the `GEI_SKIP_STATUS_CHECK` environment variable to `true`.

### Configuring multipart upload chunk size

Set the `GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES` environment variable to change the archive upload part size. Provide the value in mebibytes (MiB); For example:

```powershell
# Windows PowerShell
$env:GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES = "10"
```

```bash
# macOS/Linux
export GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES=10
```

This sets the chunk size to 10 MiB (10,485,760 bytes). The minimum supported value is 5 MiB, and the default remains 100 MiB.

This might be needed to improve upload reliability in environments with proxies or very slow connections.

## Contributions

See [Contributing](CONTRIBUTING.md) for more info on how to get involved.
2 changes: 1 addition & 1 deletion RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@

- Added support for configurable multipart upload chunk size for GitHub-owned storage uploads via `GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES` environment variable (minimum 5 MiB, default 100 MiB) to improve upload reliability in environments with proxies or slow connections
6 changes: 3 additions & 3 deletions src/Octoshift/Factories/GithubApiFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ GithubApi ISourceGithubApiFactory.Create(string apiUrl, string uploadsUrl, strin
uploadsUrl ??= DEFAULT_UPLOADS_URL;
sourcePersonalAccessToken ??= _environmentVariableProvider.SourceGithubPersonalAccessToken();
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("Default"), _versionProvider, _retryPolicy, _dateTimeProvider, sourcePersonalAccessToken);
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy);
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy, _environmentVariableProvider);
return new GithubApi(githubClient, apiUrl, _retryPolicy, multipartUploader);
}

Expand All @@ -42,7 +42,7 @@ GithubApi ISourceGithubApiFactory.CreateClientNoSsl(string apiUrl, string upload
uploadsUrl ??= DEFAULT_UPLOADS_URL;
sourcePersonalAccessToken ??= _environmentVariableProvider.SourceGithubPersonalAccessToken();
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("NoSSL"), _versionProvider, _retryPolicy, _dateTimeProvider, sourcePersonalAccessToken);
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy);
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy, _environmentVariableProvider);
return new GithubApi(githubClient, apiUrl, _retryPolicy, multipartUploader);
}

Expand All @@ -52,7 +52,7 @@ GithubApi ITargetGithubApiFactory.Create(string apiUrl, string uploadsUrl, strin
uploadsUrl ??= DEFAULT_UPLOADS_URL;
targetPersonalAccessToken ??= _environmentVariableProvider.TargetGithubPersonalAccessToken();
var githubClient = new GithubClient(_octoLogger, _clientFactory.CreateClient("Default"), _versionProvider, _retryPolicy, _dateTimeProvider, targetPersonalAccessToken);
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy);
var multipartUploader = new ArchiveUploader(githubClient, uploadsUrl, _octoLogger, _retryPolicy, _environmentVariableProvider);
return new GithubApi(githubClient, apiUrl, _retryPolicy, multipartUploader);
}
}
31 changes: 29 additions & 2 deletions src/Octoshift/Services/ArchiveUploader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,26 @@ namespace OctoshiftCLI.Services;

public class ArchiveUploader
{
private const int BYTES_PER_MEBIBYTE = 1024 * 1024;
private const int MIN_MULTIPART_MEBIBYTES = 5; // 5 MiB minimum size for multipart upload. Don't allow overrides smaller than this.
private const int DEFAULT_MULTIPART_MEBIBYTES = 100;

private readonly GithubClient _client;
private readonly string _uploadsUrl;
private readonly OctoLogger _log;
internal int _streamSizeLimit = 100 * 1024 * 1024; // 100 MiB
private readonly EnvironmentVariableProvider _environmentVariableProvider;
internal int _streamSizeLimit = DEFAULT_MULTIPART_MEBIBYTES * BYTES_PER_MEBIBYTE; // 100 MiB stored in bytes
private readonly RetryPolicy _retryPolicy;

public ArchiveUploader(GithubClient client, string uploadsUrl, OctoLogger log, RetryPolicy retryPolicy)
public ArchiveUploader(GithubClient client, string uploadsUrl, OctoLogger log, RetryPolicy retryPolicy, EnvironmentVariableProvider environmentVariableProvider)
{
_client = client;
_uploadsUrl = uploadsUrl;
_log = log;
_retryPolicy = retryPolicy;
_environmentVariableProvider = environmentVariableProvider;

SetStreamSizeLimitFromEnvironment();
}
public virtual async Task<string> Upload(Stream archiveContent, string archiveName, string orgDatabaseId)
{
Expand Down Expand Up @@ -160,4 +168,23 @@ private Uri GetNextUrl(IEnumerable<KeyValuePair<string, IEnumerable<string>>> he
}
throw new OctoshiftCliException("Location header is missing in the response, unable to retrieve next URL for multipart upload.");
}

private void SetStreamSizeLimitFromEnvironment()
{
var envValue = _environmentVariableProvider.GithubOwnedStorageMultipartMebibytes();
if (!int.TryParse(envValue, out var limitInMebibytes) || limitInMebibytes <= 0)
{
return;
}

if (limitInMebibytes < MIN_MULTIPART_MEBIBYTES)
{
_log.LogWarning($"GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES is set to {limitInMebibytes} MiB, but the minimum value is {MIN_MULTIPART_MEBIBYTES} MiB. Using default value of {DEFAULT_MULTIPART_MEBIBYTES} MiB.");
return;
}

var limitBytes = (int)((long)limitInMebibytes * BYTES_PER_MEBIBYTE);
_streamSizeLimit = limitBytes;
_log.LogInformation($"Multipart upload part size set to {limitInMebibytes} MB.");
}
}
4 changes: 4 additions & 0 deletions src/Octoshift/Services/EnvironmentVariableProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class EnvironmentVariableProvider
private const string SMB_PASSWORD = "SMB_PASSWORD";
private const string GEI_SKIP_STATUS_CHECK = "GEI_SKIP_STATUS_CHECK";
private const string GEI_SKIP_VERSION_CHECK = "GEI_SKIP_VERSION_CHECK";
private const string GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES = "GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES";

private readonly OctoLogger _logger;

Expand Down Expand Up @@ -65,6 +66,9 @@ public virtual string SkipStatusCheck(bool throwIfNotFound = false) =>
public virtual string SkipVersionCheck(bool throwIfNotFound = false) =>
GetValue(GEI_SKIP_VERSION_CHECK, throwIfNotFound);

public virtual string GithubOwnedStorageMultipartMebibytes(bool throwIfNotFound = false) =>
GetValue(GITHUB_OWNED_STORAGE_MULTIPART_MEBIBYTES, throwIfNotFound);

private string GetValue(string name, bool throwIfNotFound)
{
var value = Environment.GetEnvironmentVariable(name);
Expand Down
3 changes: 2 additions & 1 deletion src/OctoshiftCLI.IntegrationTests/BbsToGithub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public BbsToGithub(ITestOutputHelper output)
_targetGithubHttpClient = new HttpClient();
_targetGithubClient = new GithubClient(_logger, _targetGithubHttpClient, new VersionChecker(_versionClient, _logger), new RetryPolicy(_logger), new DateTimeProvider(), targetGithubToken);
var retryPolicy = new RetryPolicy(_logger);
_archiveUploader = new ArchiveUploader(_targetGithubClient, UPLOADS_URL, _logger, retryPolicy);
var environmentVariableProvider = new EnvironmentVariableProvider(_logger);
_archiveUploader = new ArchiveUploader(_targetGithubClient, UPLOADS_URL, _logger, retryPolicy, environmentVariableProvider);
_targetGithubApi = new GithubApi(_targetGithubClient, "https://api.github.com", new RetryPolicy(_logger), _archiveUploader);

_blobServiceClient = new BlobServiceClient(_azureStorageConnectionString);
Expand Down
3 changes: 2 additions & 1 deletion src/OctoshiftCLI.IntegrationTests/GhesToGithub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ public GhesToGithub(ITestOutputHelper output)

_versionClient = new HttpClient();
var retryPolicy = new RetryPolicy(logger);
_archiveUploader = new ArchiveUploader(_targetGithubClient, UPLOADS_URL, logger, retryPolicy);
var environmentVariableProvider = new EnvironmentVariableProvider(logger);

_sourceGithubHttpClient = new HttpClient();
_sourceGithubClient = new GithubClient(logger, _sourceGithubHttpClient, new VersionChecker(_versionClient, logger), new RetryPolicy(logger), new DateTimeProvider(), sourceGithubToken);
_archiveUploader = new ArchiveUploader(_sourceGithubClient, UPLOADS_URL, logger, retryPolicy, environmentVariableProvider);
_sourceGithubApi = new GithubApi(_sourceGithubClient, GHES_API_URL, new RetryPolicy(logger), _archiveUploader);

_targetGithubHttpClient = new HttpClient();
Expand Down
Loading
Loading