Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1fe142f
feat: Add URL validation for org/repo/enterprise arguments with compr…
AakashSuresh2003 Dec 3, 2025
836364a
Merge branch 'main' into fix-url-validation-1180
AakashSuresh2003 Dec 3, 2025
4387a57
feat: Add URL validation for org/repo/enterprise arguments with compr…
AakashSuresh2003 Dec 3, 2025
f21b11c
feat: Add URL validation for org/repo/enterprise arguments with compr…
AakashSuresh2003 Dec 3, 2025
786bbe5
Merge branch 'fix-url-validation-1180' of https://github.com/AakashSu…
AakashSuresh2003 Dec 3, 2025
11c53c4
Merge branch '1180-fix-url-validation' of https://github.com/AakashSu…
AakashSuresh2003 Dec 3, 2025
75dcb0e
Add URL validation with clear error messages for organization, reposi…
AakashSuresh2003 Dec 3, 2025
a061ec7
Merge branch 'main' into 1180-fix-url-validation
AakashSuresh2003 Dec 5, 2025
f0e9825
Enhance validation and error messaging in RELEASENOTES
AakashSuresh2003 Dec 5, 2025
56a43b0
Merge branch 'main' into 1180-fix-url-validation
AakashSuresh2003 Dec 5, 2025
3561fe4
Merge branch 'main' of https://github.com/AakashSuresh2003/gh-gei int…
AakashSuresh2003 Dec 8, 2025
02efd86
Merge branch 'main' into 1180-fix-url-validation
AakashSuresh2003 Dec 8, 2025
2ef9d49
Merge branch 'main' into 1180-fix-url-validation
AakashSuresh2003 Dec 8, 2025
5a2f2ff
Merge branch 'main' into 1180-fix-url-validation
AakashSuresh2003 Dec 10, 2025
2ec88b9
Merge remote changes
AakashSuresh2003 Dec 11, 2025
c622573
Merge upstream/main into 1180-fix-url-validation
AakashSuresh2003 Dec 11, 2025
998011b
Used Uri.TryCreate() for URL validation instead of custom pattern mat…
AakashSuresh2003 Dec 11, 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
1 change: 1 addition & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Added support for linux-arm64 architecture for all CLI binaries (gei, ado2gh, bbs2gh), enabling users to run GEI on ARM-based systems including MacOS with Apple Silicon inside ARC runners
- Fixed issue where alert migration commands (migrate-code-scanning-alerts, migrate-secret-alerts) required GH_PAT environment variable even when GitHub tokens were provided via command-line arguments (--github-source-pat, --github-target-pat). These commands now work correctly with CLI-only token authentication.
- 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
- Added validation to detect and return clear error messages when a URL is provided instead of a name for organization, repository, or enterprise arguments (e.g., `--github-org`, `--github-target-org`, `--source-repo`, `--github-target-enterprise`)
13 changes: 12 additions & 1 deletion src/Octoshift/Commands/CreateTeam/CreateTeamCommandArgs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace OctoshiftCLI.Commands.CreateTeam;
using OctoshiftCLI.Extensions;
using OctoshiftCLI.Services;

namespace OctoshiftCLI.Commands.CreateTeam;

public class CreateTeamCommandArgs : CommandArgs
{
Expand All @@ -8,4 +11,12 @@ public class CreateTeamCommandArgs : CommandArgs
[Secret]
public string GithubPat { get; set; }
public string TargetApiUrl { get; set; }

public override void Validate(OctoLogger log)
{
if (GithubOrg.IsUrl())
{
throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}
}
}
17 changes: 16 additions & 1 deletion src/Octoshift/Commands/DownloadLogs/DownloadLogsCommandArgs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

using OctoshiftCLI.Extensions;
using OctoshiftCLI.Services;

namespace OctoshiftCLI.Commands.DownloadLogs;

public class DownloadLogsCommandArgs : CommandArgs
Expand All @@ -11,4 +13,17 @@ public class DownloadLogsCommandArgs : CommandArgs
public string GithubPat { get; set; }
public string MigrationLogFile { get; set; }
public bool Overwrite { get; set; }

public override void Validate(OctoLogger log)
{
if (GithubOrg.IsUrl())
{
throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}

if (GithubRepo.IsUrl())
{
throw new OctoshiftCliException("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo').");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.IO;
using OctoshiftCLI.Extensions;
using OctoshiftCLI.Services;

namespace OctoshiftCLI.Commands.GenerateMannequinCsv;

Expand All @@ -10,4 +12,12 @@ public class GenerateMannequinCsvCommandArgs : CommandArgs
[Secret]
public string GithubPat { get; set; }
public string TargetApiUrl { get; set; }

public override void Validate(OctoLogger log)
{
if (GithubOrg.IsUrl())
{
throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public class GrantMigratorRoleCommandArgs : CommandArgs

public override void Validate(OctoLogger log)
{
if (GithubOrg.IsUrl())
{
throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}

ActorType = ActorType?.ToUpper();

if (ActorType is "TEAM" or "USER")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using OctoshiftCLI.Services;
using OctoshiftCLI.Extensions;
using OctoshiftCLI.Services;

namespace OctoshiftCLI.Commands.ReclaimMannequin;

Expand All @@ -17,6 +18,11 @@ public class ReclaimMannequinCommandArgs : CommandArgs
public string TargetApiUrl { get; set; }
public override void Validate(OctoLogger log)
{
if (GithubOrg.IsUrl())
{
throw new OctoshiftCliException("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}

if (string.IsNullOrEmpty(Csv) && (string.IsNullOrEmpty(MannequinUser) || string.IsNullOrEmpty(TargetUser)))
{
throw new OctoshiftCliException($"Either --csv or --mannequin-user and --target-user must be specified");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public class RevokeMigratorRoleCommandArgs : CommandArgs

public override void Validate(OctoLogger log)
{
if (GithubOrg.IsUrl())
{
throw new OctoshiftCliException($"The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}

ActorType = ActorType?.ToUpper();

if (ActorType is "TEAM" or "USER")
Expand Down
24 changes: 24 additions & 0 deletions src/Octoshift/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,29 @@ public static class StringExtensions
public static string EscapeDataString(this string value) => Uri.EscapeDataString(value);

public static byte[] ToBytes(this string s) => Encoding.UTF8.GetBytes(s);

public static bool IsUrl(this string s)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer we use standard dotnet core library to validate something like below referencing https://learn.microsoft.com/en-us/dotnet/api/system.uri?view=net-8.0

public static bool IsUrl(this string s)
{
    if (s.IsNullOrWhiteSpace())
    {
        return false;
    }

    return Uri.TryCreate(s, UriKind.Absolute, out var uri) 
        && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! The implementation now uses Uri.TryCreate() for validation, and the tests have been updated accordingly

{
if (s.IsNullOrWhiteSpace())
{
return false;
}

// Check if string starts with http:// or https://
if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
s.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return true;
}

// Check if string contains common URL patterns like domain.com/path or www.
if (s.Contains("://") || s.StartsWith("www.", StringComparison.OrdinalIgnoreCase))
{
return true;
}

// Check if it looks like a URL path (contains / and .)
return s.Contains('/') && s.Contains('.');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using FluentAssertions;
using Moq;
using OctoshiftCLI.Commands.CreateTeam;
using OctoshiftCLI.Services;
using Xunit;

namespace OctoshiftCLI.Tests.Octoshift.Commands.CreateTeam;

public class CreateTeamCommandArgsTests
{
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();

private const string GITHUB_ORG = "foo-org";
private const string TEAM_NAME = "my-team";

[Fact]
public void Validate_Throws_When_GithubOrg_Is_Url()
{
var args = new CreateTeamCommandArgs
{
GithubOrg = "http://github.com/my-org",
TeamName = TEAM_NAME
};

FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should()
.ThrowExactly<OctoshiftCliException>()
.WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}

[Fact]
public void Validate_Succeeds_With_Valid_Name()
{
var args = new CreateTeamCommandArgs
{
GithubOrg = GITHUB_ORG,
TeamName = TEAM_NAME
};

args.Validate(_mockOctoLogger.Object);

args.GithubOrg.Should().Be(GITHUB_ORG);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using FluentAssertions;
using Moq;
using OctoshiftCLI.Commands.DownloadLogs;
using OctoshiftCLI.Services;
using Xunit;

namespace OctoshiftCLI.Tests.Octoshift.Commands.DownloadLogs;

public class DownloadLogsCommandArgsTests
{
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();

private const string GITHUB_ORG = "foo-org";
private const string GITHUB_REPO = "foo-repo";

[Fact]
public void Validate_Throws_When_GithubOrg_Is_Url()
{
var args = new DownloadLogsCommandArgs
{
GithubOrg = "https://github.com/my-org",
GithubRepo = GITHUB_REPO
};

FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should()
.ThrowExactly<OctoshiftCliException>()
.WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}

[Fact]
public void Validate_Throws_When_GithubRepo_Is_Url()
{
var args = new DownloadLogsCommandArgs
{
GithubOrg = GITHUB_ORG,
GithubRepo = "github.com/org/repo"
};

FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should()
.ThrowExactly<OctoshiftCliException>()
.WithMessage("The --github-repo option expects a repository name, not a URL. Please provide just the repository name (e.g., 'my-repo' instead of 'https://github.com/my-org/my-repo').");
}

[Fact]
public void Validate_Succeeds_With_Valid_Names()
{
var args = new DownloadLogsCommandArgs
{
GithubOrg = GITHUB_ORG,
GithubRepo = GITHUB_REPO
};

args.Validate(_mockOctoLogger.Object);

args.GithubOrg.Should().Be(GITHUB_ORG);
args.GithubRepo.Should().Be(GITHUB_REPO);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using FluentAssertions;
using Moq;
using OctoshiftCLI.Commands.GenerateMannequinCsv;
using OctoshiftCLI.Services;
using Xunit;

namespace OctoshiftCLI.Tests.Octoshift.Commands.GenerateMannequinCsv;

public class GenerateMannequinCsvCommandArgsTests
{
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();

private const string GITHUB_ORG = "foo-org";

[Fact]
public void Validate_Throws_When_GithubOrg_Is_Url()
{
var args = new GenerateMannequinCsvCommandArgs
{
GithubOrg = "www.github.com"
};

FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should()
.ThrowExactly<OctoshiftCliException>()
.WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}

[Fact]
public void Validate_Succeeds_With_Valid_Name()
{
var args = new GenerateMannequinCsvCommandArgs
{
GithubOrg = GITHUB_ORG
};

args.Validate(_mockOctoLogger.Object);

args.GithubOrg.Should().Be(GITHUB_ORG);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,20 @@ public void It_Validates_GhesApiUrl_And_TargetApiUrl()
FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should().Throw<OctoshiftCliException>();
}

[Fact]
public void Validate_Throws_When_GithubOrg_Is_Url()
{
var args = new GrantMigratorRoleCommandArgs
{
GithubOrg = "https://github.com/my-org",
Actor = ACTOR,
ActorType = "USER"
};

FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should()
.ThrowExactly<OctoshiftCliException>()
.WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,19 @@ public void No_Parameters_Provided_Throws_OctoshiftCliException()
.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should().Throw<OctoshiftCliException>();
}

[Fact]
public void Validate_Throws_When_GithubOrg_Is_Url()
{
var args = new ReclaimMannequinCommandArgs
{
GithubOrg = "www.github.com/my-org",
Csv = "mannequins.csv"
};

FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should()
.ThrowExactly<OctoshiftCliException>()
.WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,20 @@ public void It_Validates_GhesApiUrl_And_TargetApiUrl()
.Should()
.ThrowExactly<OctoshiftCliException>();
}

[Fact]
public void Validate_Throws_When_GithubOrg_Is_Url()
{
var args = new RevokeMigratorRoleCommandArgs
{
GithubOrg = "github.com/my-org",
Actor = ACTOR,
ActorType = "USER"
};

FluentActions.Invoking(() => args.Validate(_mockOctoLogger.Object))
.Should()
.ThrowExactly<OctoshiftCliException>()
.WithMessage("The --github-org option expects an organization name, not a URL. Please provide just the organization name (e.g., 'my-org' instead of 'https://github.com/my-org').");
}
}
22 changes: 22 additions & 0 deletions src/OctoshiftCLI.Tests/StringExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,27 @@ public void ReplaceInvalidCharactersWithDash_Returns_Valid_String(string value,

normalizedValue.Should().Be(expectedValue);
}

[Theory]
[InlineData("https://github.com/my-org", true)]
[InlineData("http://github.com/my-org", true)]
[InlineData("https://github.com/my-org/my-repo", true)]
[InlineData("http://example.com", true)]
[InlineData("www.github.com", true)]
[InlineData("github.com/my-org", true)]
[InlineData("my-org", false)]
[InlineData("my-repo", false)]
[InlineData("my-org-123", false)]
[InlineData("my_repo", false)]
[InlineData("MyOrganization", false)]
[InlineData("", false)]
[InlineData(null, false)]
[InlineData(" ", false)]
public void IsUrl_Detects_URLs_Correctly(string value, bool expectedResult)
{
var result = value.IsUrl();

result.Should().Be(expectedResult);
}
}
}
Loading
Loading