From e6b23a41b42a6ca293e0d36815ab6d8bbfbc0f48 Mon Sep 17 00:00:00 2001 From: Fernando Salomon Date: Mon, 14 Sep 2026 10:15:08 -0400 Subject: [PATCH 1/7] Add support for GitLab v19 --- .github/dependabot.yml | 3 - .github/workflows/ci.yml | 2 +- Directory.Packages.props | 1 - NGitLab.Tests/Docker/GitLabCredential.cs | 6 +- NGitLab.Tests/Docker/GitLabDockerContainer.cs | 306 +++++------------- NGitLab.Tests/Docker/GitLabTestContext.cs | 2 +- NGitLab.Tests/NGitLab.Tests.csproj | 1 - 7 files changed, 83 insertions(+), 238 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6d185fcff..c2cc4b95a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -41,12 +41,10 @@ updates: exclude-patterns: - "*Analyzer*" - "Microsoft.NET.Test.Sdk" - - "Microsoft.Playwright" test-dependencies: patterns: - "coverlet.collector" - "Microsoft.NET.Test.Sdk" - - "Microsoft.Playwright" - "NSubstitute" - "*NUnit*" exclude-patterns: @@ -60,6 +58,5 @@ updates: - "System.*" - "coverlet.collector" - "Microsoft.NET.Test.Sdk" - - "Microsoft.Playwright" - "NSubstitute" - "*NUnit*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a81b4cb1c..5dc13ea03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,8 @@ jobs: gitlab: # Keep in sync with the version in GitLabDockerContainer.cs # Available tags: https://hub.docker.com/r/gitlab/gitlab-ee/tags?name=-ee.0 - - "gitlab/gitlab-ee:17.1.8-ee.0" - "gitlab/gitlab-ee:18.1.6-ee.0" + - "gitlab/gitlab-ee:19.3.1-ee.0" configuration: [Release] fail-fast: false services: diff --git a/Directory.Packages.props b/Directory.Packages.props index 2737384a8..61304d1d0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,6 @@ - diff --git a/NGitLab.Tests/Docker/GitLabCredential.cs b/NGitLab.Tests/Docker/GitLabCredential.cs index 9e9d743d6..a9444c2df 100644 --- a/NGitLab.Tests/Docker/GitLabCredential.cs +++ b/NGitLab.Tests/Docker/GitLabCredential.cs @@ -1,12 +1,8 @@ -namespace NGitLab.Tests.Docker; +namespace NGitLab.Tests.Docker; public record GitLabCredential { public string AdminUserToken { get; set; } - public string AdminCookies { get; set; } - public string UserToken { get; set; } - - public string ProfileToken { get; set; } } diff --git a/NGitLab.Tests/Docker/GitLabDockerContainer.cs b/NGitLab.Tests/Docker/GitLabDockerContainer.cs index 542909494..cd3a455f5 100644 --- a/NGitLab.Tests/Docker/GitLabDockerContainer.cs +++ b/NGitLab.Tests/Docker/GitLabDockerContainer.cs @@ -13,9 +13,7 @@ using System.Threading.Tasks; using Docker.DotNet; using Docker.DotNet.Models; -using Microsoft.Playwright; using NGitLab.Models; -using NuGet.Versioning; using NUnit.Framework; using Polly; @@ -34,12 +32,7 @@ public class GitLabDockerContainer /// Keep in sync with .github/workflows/ci.yml, use the lowest supported version /// List of available versions: https://hub.docker.com/r/gitlab/gitlab-ee/tags/ /// - private const string LocalGitLabDockerVersion = "18.1.6-ee.0"; - - /// - /// Resolved GitLab version taken from the help page once logged in - /// - private static string ResolvedGitLabVersion; + private const string LocalGitLabDockerVersion = "19.3.1-ee.0"; private static string s_creationErrorMessage; private static readonly SemaphoreSlim s_setupLock = new(initialCount: 1, maxCount: 1); @@ -113,21 +106,7 @@ private async Task SetupAsync() await SpawnDockerContainerAsync().ConfigureAwait(false); } - EnsureChromiumIsInstalled(); - - // Use Playwright to launch Chromium - using var playwright = await Playwright.CreateAsync(); - await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions - { - // Headless = false, // Uncomment to have browser window visible - // SlowMo = 1000, // Slows down Playwright operations by the specified amount of ms. - }); - await using var browserContext = await browser.NewContextAsync(); - - await LoginAsync(browserContext); - await ResolveGitLabVersionAsync(browserContext).ConfigureAwait(false); - - await LoadCredentialsAsync().ConfigureAwait(false); + LoadCredentials(); if (Credentials != null) { @@ -135,19 +114,8 @@ private async Task SetupAsync() return; } - await GenerateCredentialsAsync(browserContext).ConfigureAwait(false); + await GenerateCredentialsAsync().ConfigureAwait(false); PersistCredentialsAsync(); - - static void EnsureChromiumIsInstalled() - { - TestContext.Progress.WriteLine("Making sure Chromium is installed"); - - var exitCode = Microsoft.Playwright.Program.Main(new[] { "install", "--force", "chromium", "--with-deps" }); - if (exitCode != 0) - throw new InvalidOperationException($"Cannot install browser (exit code: {exitCode})"); - - TestContext.Progress.WriteLine("Chromium installed"); - } } private static async Task ValidateDockerIsEnabled(DockerClient client) @@ -316,7 +284,7 @@ private async Task SpawnDockerContainerAsync() TestContext.Progress.WriteLine("GitLab Docker container is ready"); } - private async Task GenerateCredentialsAsync(IBrowserContext browserContext) + private async Task GenerateCredentialsAsync() { Console.WriteLine("Requesting credentials from GitLab instance"); @@ -333,84 +301,31 @@ async Task GenerateAdminToken(GitLabCredential credentials) { TestContext.Progress.WriteLine("Generating Credentials"); - var gitLabVersionAsNuGetVersion = NuGetVersion.Parse(ResolvedGitLabVersion); - var isMajorVersion15 = VersionRange.Parse("[15.0,16.0)").Satisfies(gitLabVersionAsNuGetVersion); - var isMajorVersionAtLeast16 = VersionRange.Parse("[16.0,)").Satisfies(gitLabVersionAsNuGetVersion); - var isMajorVersionAtLeast18 = VersionRange.Parse("[18.0,)").Satisfies(gitLabVersionAsNuGetVersion); - - TestContext.Progress.WriteLine("Creating root token"); - - var accessTokenRelativeUri = "/-/profile/personal_access_tokens"; - if (isMajorVersionAtLeast18) - { - accessTokenRelativeUri = "/-/user_settings/personal_access_tokens"; - } - - var page = await browserContext.NewPageAsync(); - await page.GotoAsync(new Uri(GitLabUrl, accessTokenRelativeUri).ToString()); - - var formLocator = page.Locator("main#content-body form"); - - var tokenName = "GitLabClientTest-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); - - if (isMajorVersionAtLeast18) - { - await page.Locator("main[id='content-body'] button[data-testid='add-new-token-button']").ClickAsync(new LocatorClickOptions { Timeout = 5_000 }); - formLocator = page.Locator("form[id='token-create-form']"); - await formLocator.Locator("input[data-testid='access-token-name-field']").FillAsync(tokenName); - } - else if (isMajorVersionAtLeast16) - { - await SkipVersionReminder(page); - - await page.Locator("main[id='content-body'] button[data-testid='add-new-token-button']").ClickAsync(new LocatorClickOptions { Timeout = 5_000 }); - formLocator = page.Locator("main[id='content-body'] form[id='js-new-access-token-form']"); - await formLocator.Locator("input[data-testid='access-token-name-field']").FillAsync(tokenName); - } - else if (isMajorVersion15) - { - // Try the "old" 15.x.y way - formLocator = page.Locator("main#content-body form"); - await formLocator.GetByLabel("Token name").FillAsync(tokenName); - } - else - { - s_creationErrorMessage = $"Unable to generate an admin token: resolved GitLab version '{ResolvedGitLabVersion}' doesn't match any supported range in '{nameof(GenerateCredentialsAsync)}'."; - Assert.Fail(s_creationErrorMessage); - } - - foreach (var checkbox in await formLocator.GetByRole(AriaRole.Checkbox).AllAsync()) - { - await checkbox.CheckAsync(new LocatorCheckOptions { Force = true }); - } - - string token = null; - if (isMajorVersionAtLeast18) - { - await formLocator.GetByTestId("create-token-button").ClickAsync(); - await page.GetByRole(AriaRole.Alert).GetByLabel("Click to reveal").ClickAsync(); - token = await page.GetByTestId("created-access-token-field").InputValueAsync(); - } - else + using var conf = new DockerClientConfiguration(new Uri(OperatingSystem.IsWindows() ? "npipe://./pipe/docker_engine" : "unix:///var/run/docker.sock")); + using var client = conf.CreateClient(); + await ValidateDockerIsEnabled(client).ConfigureAwait(false); + + TestContext.Progress.WriteLine("Creating root token via 'gitlab-rails runner'"); + + // Keep only scopes the running GitLab version supports (an unknown scope makes `create!` raise). + const string script = """ + desired_scopes = %w[api read_user read_api read_repository write_repository sudo admin_mode create_runner manage_runner k8s_proxy] + available_scopes = Gitlab::Auth.all_available_scopes.map(&:to_s) + token = User.find_by_username!('root').personal_access_tokens.create!( + name: 'NGitLabClientTest', + scopes: (desired_scopes & available_scopes), + expires_at: 1.year.from_now) + puts token.token + """; + + var retryPolicy = Policy.Handle().WaitAndRetryAsync(20, _ => TimeSpan.FromSeconds(3)); + var token = await retryPolicy.ExecuteAsync(async () => { - await formLocator.GetByRole(AriaRole.Button, new() { Name = "Create personal access token" }).ClickAsync(); - token = await page.Locator("button[title='Copy personal access token']").GetAttributeAsync("data-clipboard-text"); - } + var containerId = await ResolveGitLabContainerIdAsync(client).ConfigureAwait(false); + return await RunGitLabRailsRunnerAsync(client, containerId, script).ConfigureAwait(false); + }).ConfigureAwait(false); credentials.AdminUserToken = token; - - // Get admin login cookie - // result.Cookie: experimentation_subject_id=XXX; _gitlab_session=XXXX; known_sign_in=XXXX - TestContext.Progress.WriteLine("Extracting GitLab session cookie"); - var cookies = await browserContext.CookiesAsync(new[] { GitLabUrl.AbsoluteUri }); - foreach (var cookie in cookies) - { - if (cookie.Name == "_gitlab_session") - { - credentials.AdminCookies = cookie.Value; - break; - } - } } void GenerateUserToken() @@ -454,15 +369,48 @@ void GenerateUserToken() } } - private static async Task SkipVersionReminder(IPage page) + private static async Task ResolveGitLabContainerIdAsync(DockerClient client) { - try + var containers = await client.Containers.ListContainersAsync(new ContainersListParameters { All = true }).ConfigureAwait(false); + + var container = containers.FirstOrDefault(c => c.Names.Contains("/" + ContainerName, StringComparer.Ordinal)) + ?? containers.FirstOrDefault(c => c.Image.StartsWith(ImageName, StringComparison.Ordinal)); + + if (container == null) + throw new InvalidOperationException($"Cannot find a running Docker container for image '{ImageName}' to generate credentials from."); + + return container.ID; + } + + private static async Task RunGitLabRailsRunnerAsync(DockerClient client, string containerId, string script) + { + var execCreateResponse = await client.Exec.ExecCreateContainerAsync(containerId, new ContainerExecCreateParameters { - await page.Locator("button[data-testid='alert-modal-remind-button']").ClickAsync(new LocatorClickOptions { Timeout = 3_000 }); - } - catch (Exception) + AttachStdout = true, + AttachStderr = true, + Cmd = ["gitlab-rails", "runner", script], + }).ConfigureAwait(false); + + string stdout; + string stderr; + using (var stream = await client.Exec.StartAndAttachContainerExecAsync(execCreateResponse.ID, tty: false).ConfigureAwait(false)) { + (stdout, stderr) = await stream.ReadOutputToEndAsync(CancellationToken.None).ConfigureAwait(false); } + + var inspectResponse = await client.Exec.InspectContainerExecAsync(execCreateResponse.ID).ConfigureAwait(false); + if (inspectResponse.ExitCode != 0) + throw new InvalidOperationException($"'gitlab-rails runner' failed with exit code {inspectResponse.ExitCode}.\nStdout: {stdout}\nStderr: {stderr}"); + + var token = stdout + .Split('\n') + .Select(line => line.Trim()) + .LastOrDefault(line => line.Length > 0); + + if (string.IsNullOrEmpty(token)) + throw new InvalidOperationException($"'gitlab-rails runner' did not output a token.\nStdout: {stdout}\nStderr: {stderr}"); + + return token; } private void PersistCredentialsAsync() @@ -473,40 +421,26 @@ private void PersistCredentialsAsync() File.WriteAllText(path, json); } - private async Task LoadCredentialsAsync() + private void LoadCredentials() { var file = GetCredentialsFilePath(); - if (File.Exists(file)) - { - var json = File.ReadAllText(file); - var credentials = JsonSerializer.Deserialize(json); - if (credentials.AdminUserToken == null || credentials.UserToken == null) - return; + if (!File.Exists(file)) + return; - var client = new GitLabClient(GitLabUrl.ToString(), credentials.AdminUserToken); - try - { - // Validate token - var user = client.Users.Current; + var json = File.ReadAllText(file); + var credentials = JsonSerializer.Deserialize(json); + if (credentials.AdminUserToken == null || credentials.UserToken == null) + return; - using var httpClient = new HttpClient - { - BaseAddress = GitLabUrl, - DefaultRequestHeaders = - { - { "Cookie", "_gitlab_session=" + credentials.AdminCookies }, - }, - }; - var response = await httpClient.GetAsync(new Uri("/", UriKind.RelativeOrAbsolute)); - if (response.RequestMessage.RequestUri.PathAndQuery == "/users/sign_in") - return; - - // Validate cookie - Credentials = credentials; - } - catch (GitLabException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) - { - } + var client = new GitLabClient(GitLabUrl.ToString(), credentials.AdminUserToken); + try + { + // Validate token + _ = client.Users.Current; + Credentials = credentials; + } + catch (GitLabException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) + { } } @@ -540,84 +474,4 @@ private async Task WaitForCiGitLabInstance() s_creationErrorMessage = "GitLab is not well configured in CI"; Assert.Fail(s_creationErrorMessage); } - - private async Task ResolveGitLabVersionAsync(IBrowserContext browserContext) - { - Console.WriteLine("Resolving GitLab version from help page..."); - var page = await browserContext.NewPageAsync(); - await page.GotoAsync(new Uri(GitLabUrl, "help").AbsoluteUri); - var titleLink = await page.QuerySelectorAsync("h1 a"); - - if (titleLink is null) - { - s_creationErrorMessage = "Cannot find title on the help page to get GitLab version"; - Assert.Fail(s_creationErrorMessage); - } - - var version = await titleLink.TextContentAsync(); - - if (string.IsNullOrEmpty(version)) - { - s_creationErrorMessage = "Found title on the help page, but the version is empty"; - Assert.Fail(s_creationErrorMessage); - } - - ResolvedGitLabVersion = version.Trim().TrimStart('v'); - Console.WriteLine($"GitLab resolved version is '{ResolvedGitLabVersion}'"); - - await CloseRedesignModal(page); - } - - private async Task LoginAsync(IBrowserContext browserContext) - { - var page = await browserContext.NewPageAsync(); - await page.GotoAsync(GitLabUrl.AbsoluteUri); - var url = await GetCurrentUrl(page); - - if (url != "/users/sign_in") - { - Console.WriteLine("Already logged in on GitLab instance"); - return; - } - - Console.WriteLine("Logging in on GitLab instance..."); - - var v15LoginInput = "form#new_user input[name='user[login]']"; - var v16LoginInput = "form[data-testid='sign-in-form'] input[name='user[login]']"; - - if (await page.QuerySelectorAsync(v15LoginInput) is not null) - { - await page.Locator(v15LoginInput).FillAsync(AdminUserName); - await page.Locator("form#new_user input[name='user[password]']").FillAsync(AdminPassword); - } - else if (await page.QuerySelectorAsync(v16LoginInput) is not null) - { - await page.Locator(v16LoginInput).FillAsync(AdminUserName); - await page.Locator("form[data-testid='sign-in-form'] input[name='user[password]']").FillAsync(AdminPassword); - } - else - { - s_creationErrorMessage = $"Unable to find the correct login input. Please make sure that login form for the GitLab version you target is supported in '{nameof(LoginAsync)}'"; - Assert.Fail(s_creationErrorMessage); - } - - var checkbox = page.Locator("form[data-testid='sign-in-form'] input[type=checkbox][name='user[remember_me]']"); - await checkbox.CheckAsync(new LocatorCheckOptions { Force = true }); - - await page.RunAndWaitForResponseAsync(async () => - { - await page.EvalOnSelectorAsync("form[data-testid='sign-in-form']", "form => form.submit()"); - }, response => response.Status == 200); - } - - private async Task CloseRedesignModal(IPage page) - { - var isModalVisible = await page.IsVisibleAsync("div#dap_welcome_modal button[aria-label='Close']"); - if (isModalVisible) - { - await page.Locator("div#dap_welcome_modal button[aria-label='Close']").ClickAsync(); - } - } - - private static Task GetCurrentUrl(IPage page) => page.EvaluateAsync("window.location.pathname"); } diff --git a/NGitLab.Tests/Docker/GitLabTestContext.cs b/NGitLab.Tests/Docker/GitLabTestContext.cs index 0bb96a139..7472a12ac 100644 --- a/NGitLab.Tests/Docker/GitLabTestContext.cs +++ b/NGitLab.Tests/Docker/GitLabTestContext.cs @@ -52,7 +52,7 @@ private GitLabTestContext(GitLabDockerContainer container) BaseAddress = DockerContainer.GitLabUrl, DefaultRequestHeaders = { - { "Cookie", "_gitlab_session=" + DockerContainer.Credentials.AdminCookies }, + { "Authorization", "Bearer " + DockerContainer.Credentials.AdminUserToken }, }, }; } diff --git a/NGitLab.Tests/NGitLab.Tests.csproj b/NGitLab.Tests/NGitLab.Tests.csproj index 3aa6838a1..4e353f38f 100644 --- a/NGitLab.Tests/NGitLab.Tests.csproj +++ b/NGitLab.Tests/NGitLab.Tests.csproj @@ -9,7 +9,6 @@ - From 9682fdfbbd88b9e696e1e6e8b6770ad98764c36a Mon Sep 17 00:00:00 2001 From: Fernando Salomon Date: Mon, 14 Sep 2026 15:19:01 -0400 Subject: [PATCH 2/7] Add AllowEmpty to fix when Actions are empty --- NGitLab.Tests/CommitsTests.cs | 1 + NGitLab.Tests/ContributorsTests.cs | 1 + NGitLab/Models/CommitCreate.cs | 3 +++ 3 files changed, 5 insertions(+) diff --git a/NGitLab.Tests/CommitsTests.cs b/NGitLab.Tests/CommitsTests.cs index 01440a79f..200f9d11e 100644 --- a/NGitLab.Tests/CommitsTests.cs +++ b/NGitLab.Tests/CommitsTests.cs @@ -81,6 +81,7 @@ public async Task Test_can_get_merge_request_associated_to_commit() { Branch = "test-mr", CommitMessage = "Test to retrieve MR from commit sha", + AllowEmpty = true, }); var mergeRequestTitle = "Test to retrieve MR from commit sha"; diff --git a/NGitLab.Tests/ContributorsTests.cs b/NGitLab.Tests/ContributorsTests.cs index fcc5dc48e..d049d58fc 100644 --- a/NGitLab.Tests/ContributorsTests.cs +++ b/NGitLab.Tests/ContributorsTests.cs @@ -59,6 +59,7 @@ public async Task Test_can_get_MultipleContributors() Branch = project.DefaultBranch, StartBranch = project.DefaultBranch, CommitMessage = "test", + AllowEmpty = true, }); var contributors = await GitLabTestContext.RetryUntilAsync(() => contributorsClient.All.ToList(), c => c.Count >= 2, TimeSpan.FromMinutes(2)); diff --git a/NGitLab/Models/CommitCreate.cs b/NGitLab/Models/CommitCreate.cs index ee79c0610..54c62d50a 100644 --- a/NGitLab/Models/CommitCreate.cs +++ b/NGitLab/Models/CommitCreate.cs @@ -30,6 +30,9 @@ public class CommitCreate [JsonPropertyName("force")] public bool? Force { get; set; } + + [JsonPropertyName("allow_empty")] + public bool? AllowEmpty { get; set; } } public class CreateCommitAction From 094a0e8f3ad735dba684816c852092a74c60a909 Mon Sep 17 00:00:00 2001 From: Fernando Salomon Date: Mon, 14 Sep 2026 15:27:56 -0400 Subject: [PATCH 3/7] Fix GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorrectly get the head commit instead of the first one --- NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs index e6536ef17..169da7c87 100644 --- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs +++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs @@ -478,13 +478,13 @@ public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorr { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); - var firstCommitId = context.Commits[0].Id.ToString(); + var lastCommitId = context.Commits[1].Id.ToString(); var path = RepositoryClientTestsContext.SubfolderName; var fileArchiveQuery = new FileArchiveQuery { Format = FileArchiveFormat.Zip, Path = path, - Ref = firstCommitId, + Ref = lastCommitId, }; // Act @@ -498,7 +498,7 @@ public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorr Assert.That(requestPathAndQuery, Is.Not.Null); Assert.That(requestPathAndQuery.Contains($"/archive.zip", StringComparison.OrdinalIgnoreCase), Is.True); Assert.That(requestPathAndQuery.Contains($"path={path}", StringComparison.OrdinalIgnoreCase), Is.True); - Assert.That(requestPathAndQuery.Contains($"sha={firstCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.Contains($"sha={lastCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); } } From 2ac90fbe31e34c9b2e94467fb585f4b7e5116de1 Mon Sep 17 00:00:00 2001 From: Fernando Salomon Date: Mon, 14 Sep 2026 15:31:50 -0400 Subject: [PATCH 4/7] Add Allow Empty to public API --- NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 8a26a2a51..9d6818b93 100644 --- a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1565,6 +1565,7 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void NGitLab.Models.CommitCreate NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList NGitLab.Models.CommitCreate.Actions.set -> void +NGitLab.Models.CommitCreate.AllowEmpty.get -> bool? NGitLab.Models.CommitCreate.AuthorEmail.get -> string NGitLab.Models.CommitCreate.AuthorEmail.set -> void NGitLab.Models.CommitCreate.AuthorName.get -> string From 8c84a7bf616234baaf164db2a430b7ca78e708ee Mon Sep 17 00:00:00 2001 From: Fernando Salomon Date: Mon, 14 Sep 2026 15:46:10 -0400 Subject: [PATCH 5/7] Update publicApi for net10, net472, net8 and netstandard2.0 --- NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt | 2 ++ NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt | 2 ++ NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt | 2 ++ NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | 1 + 4 files changed, 7 insertions(+) diff --git a/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 8bfcb4d7f..398a4b865 100644 --- a/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1564,6 +1564,8 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void NGitLab.Models.CommitCreate NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList NGitLab.Models.CommitCreate.Actions.set -> void +NGitLab.Models.CommitCreate.AllowEmpty.get -> bool? +NGitLab.Models.CommitCreate.AllowEmpty.set -> void NGitLab.Models.CommitCreate.AuthorEmail.get -> string NGitLab.Models.CommitCreate.AuthorEmail.set -> void NGitLab.Models.CommitCreate.AuthorName.get -> string diff --git a/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt index 8a26a2a51..371bc878c 100644 --- a/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1565,6 +1565,8 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void NGitLab.Models.CommitCreate NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList NGitLab.Models.CommitCreate.Actions.set -> void +NGitLab.Models.CommitCreate.AllowEmpty.get -> bool? +NGitLab.Models.CommitCreate.AllowEmpty.set -> void NGitLab.Models.CommitCreate.AuthorEmail.get -> string NGitLab.Models.CommitCreate.AuthorEmail.set -> void NGitLab.Models.CommitCreate.AuthorName.get -> string diff --git a/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 8bfcb4d7f..398a4b865 100644 --- a/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1564,6 +1564,8 @@ NGitLab.Models.CommitCherryPick.Sha.set -> void NGitLab.Models.CommitCreate NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList NGitLab.Models.CommitCreate.Actions.set -> void +NGitLab.Models.CommitCreate.AllowEmpty.get -> bool? +NGitLab.Models.CommitCreate.AllowEmpty.set -> void NGitLab.Models.CommitCreate.AuthorEmail.get -> string NGitLab.Models.CommitCreate.AuthorEmail.set -> void NGitLab.Models.CommitCreate.AuthorName.get -> string diff --git a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 9d6818b93..371bc878c 100644 --- a/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1566,6 +1566,7 @@ NGitLab.Models.CommitCreate NGitLab.Models.CommitCreate.Actions.get -> System.Collections.Generic.IList NGitLab.Models.CommitCreate.Actions.set -> void NGitLab.Models.CommitCreate.AllowEmpty.get -> bool? +NGitLab.Models.CommitCreate.AllowEmpty.set -> void NGitLab.Models.CommitCreate.AuthorEmail.get -> string NGitLab.Models.CommitCreate.AuthorEmail.set -> void NGitLab.Models.CommitCreate.AuthorName.get -> string From 68fcd93a54f87172f59c7e9002fded2856201ede Mon Sep 17 00:00:00 2001 From: Thomas Cortes Date: Wed, 16 Sep 2026 11:23:46 -0400 Subject: [PATCH 6/7] fix tests --- NGitLab.Tests/CommitsTests.cs | 9 +++++++-- NGitLab.Tests/ContributorsTests.cs | 10 ++++++++-- NGitLab.Tests/Docker/GitLabDockerContainer.cs | 2 +- .../RepositoryClient/RepositoryClientTests.cs | 11 ++++++----- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/NGitLab.Tests/CommitsTests.cs b/NGitLab.Tests/CommitsTests.cs index 200f9d11e..ed100a4da 100644 --- a/NGitLab.Tests/CommitsTests.cs +++ b/NGitLab.Tests/CommitsTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using NGitLab.Models; using NGitLab.Tests.Docker; +using NuGet.Versioning; using NUnit.Framework; namespace NGitLab.Tests; @@ -70,9 +71,13 @@ public async Task Test_can_get_stats_in_commit() [Test] [NGitLabRetry] - public async Task Test_can_get_merge_request_associated_to_commit() + [TestCase("(,19.0.0)", null)] // v18 allowed empty commits by default + [TestCase("[19.0.0,)", true)] // v19 only allow empty commits if specified + public async Task Test_can_get_merge_request_associated_to_commit(string versionRange, bool? allowEmpty) { using var context = await GitLabTestContext.CreateAsync(); + context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse(versionRange)); + var project = context.CreateProject(); context.Client.GetRepository(project.Id).Branches.Create(new BranchCreate { Name = "test-mr", Ref = project.DefaultBranch }); @@ -81,7 +86,7 @@ public async Task Test_can_get_merge_request_associated_to_commit() { Branch = "test-mr", CommitMessage = "Test to retrieve MR from commit sha", - AllowEmpty = true, + AllowEmpty = allowEmpty, }); var mergeRequestTitle = "Test to retrieve MR from commit sha"; diff --git a/NGitLab.Tests/ContributorsTests.cs b/NGitLab.Tests/ContributorsTests.cs index d049d58fc..4db053ccc 100644 --- a/NGitLab.Tests/ContributorsTests.cs +++ b/NGitLab.Tests/ContributorsTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using NGitLab.Models; using NGitLab.Tests.Docker; +using NuGet.Versioning; using NUnit.Framework; namespace NGitLab.Tests; @@ -25,9 +26,14 @@ public async Task Test_can_get_contributors() [Test] [NGitLabRetry] - public async Task Test_can_get_MultipleContributors() + + [TestCase("(,19.0.0)", null)] // v18 allowed empty commits by default + [TestCase("[19.0.0,)", true)] // v19 only allow empty commits if specified + public async Task Test_can_get_MultipleContributors(string versionRange, bool? allowEmpty) { using var context = await GitLabTestContext.CreateAsync(); + context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse(versionRange)); + var project = context.CreateProject(initializeWithCommits: true); var contributorsClient = context.Client.GetRepository(project.Id).Contributors; var currentUser = context.Client.Users.Current; @@ -59,7 +65,7 @@ public async Task Test_can_get_MultipleContributors() Branch = project.DefaultBranch, StartBranch = project.DefaultBranch, CommitMessage = "test", - AllowEmpty = true, + AllowEmpty = allowEmpty, }); var contributors = await GitLabTestContext.RetryUntilAsync(() => contributorsClient.All.ToList(), c => c.Count >= 2, TimeSpan.FromMinutes(2)); diff --git a/NGitLab.Tests/Docker/GitLabDockerContainer.cs b/NGitLab.Tests/Docker/GitLabDockerContainer.cs index cd3a455f5..13aab36d3 100644 --- a/NGitLab.Tests/Docker/GitLabDockerContainer.cs +++ b/NGitLab.Tests/Docker/GitLabDockerContainer.cs @@ -32,7 +32,7 @@ public class GitLabDockerContainer /// Keep in sync with .github/workflows/ci.yml, use the lowest supported version /// List of available versions: https://hub.docker.com/r/gitlab/gitlab-ee/tags/ /// - private const string LocalGitLabDockerVersion = "19.3.1-ee.0"; + private const string LocalGitLabDockerVersion = "18.1.6-ee.0"; private static string s_creationErrorMessage; private static readonly SemaphoreSlim s_setupLock = new(initialCount: 1, maxCount: 1); diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs index 169da7c87..99cbc5813 100644 --- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs +++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs @@ -474,17 +474,18 @@ public async Task GetArchive_QuerySpecifiesPath_PathValuePassedCorrectly() [Test] [NGitLabRetry] - public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorrectly() + [TestCase("", 0)] + [TestCase(RepositoryClientTestsContext.SubfolderName, 1)] + public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorrectly(string path, int commitIndex) { // Arrange using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2); - var lastCommitId = context.Commits[1].Id.ToString(); - var path = RepositoryClientTestsContext.SubfolderName; + var commit = context.Commits[commitIndex].Id.ToString(); var fileArchiveQuery = new FileArchiveQuery { Format = FileArchiveFormat.Zip, Path = path, - Ref = lastCommitId, + Ref = commit, }; // Act @@ -498,7 +499,7 @@ public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorr Assert.That(requestPathAndQuery, Is.Not.Null); Assert.That(requestPathAndQuery.Contains($"/archive.zip", StringComparison.OrdinalIgnoreCase), Is.True); Assert.That(requestPathAndQuery.Contains($"path={path}", StringComparison.OrdinalIgnoreCase), Is.True); - Assert.That(requestPathAndQuery.Contains($"sha={lastCommitId}", StringComparison.OrdinalIgnoreCase), Is.True); + Assert.That(requestPathAndQuery.Contains($"sha={commit}", StringComparison.OrdinalIgnoreCase), Is.True); } } From dacd2858726fef29574d614f1401ae41fc6b1b11 Mon Sep 17 00:00:00 2001 From: Thomas Cortes Date: Wed, 16 Sep 2026 11:33:34 -0400 Subject: [PATCH 7/7] fix test --- NGitLab.Tests/Docker/GitLabDockerContainer.cs | 2 +- NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/NGitLab.Tests/Docker/GitLabDockerContainer.cs b/NGitLab.Tests/Docker/GitLabDockerContainer.cs index 13aab36d3..cd3a455f5 100644 --- a/NGitLab.Tests/Docker/GitLabDockerContainer.cs +++ b/NGitLab.Tests/Docker/GitLabDockerContainer.cs @@ -32,7 +32,7 @@ public class GitLabDockerContainer /// Keep in sync with .github/workflows/ci.yml, use the lowest supported version /// List of available versions: https://hub.docker.com/r/gitlab/gitlab-ee/tags/ /// - private const string LocalGitLabDockerVersion = "18.1.6-ee.0"; + private const string LocalGitLabDockerVersion = "19.3.1-ee.0"; private static string s_creationErrorMessage; private static readonly SemaphoreSlim s_setupLock = new(initialCount: 1, maxCount: 1); diff --git a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs index 99cbc5813..4a0601275 100644 --- a/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs +++ b/NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs @@ -474,8 +474,8 @@ public async Task GetArchive_QuerySpecifiesPath_PathValuePassedCorrectly() [Test] [NGitLabRetry] - [TestCase("", 0)] - [TestCase(RepositoryClientTestsContext.SubfolderName, 1)] + [TestCase("", 0)] // First commit creates a file on root + [TestCase(RepositoryClientTestsContext.SubfolderName, 1)] // Second commit creates a file on a subfolder public async Task GetArchive_QuerySpecifiesAllParameters_AllParametersPassedCorrectly(string path, int commitIndex) { // Arrange