From 65cbfc9832d4692b74db89edf99a6f6ec82908a0 Mon Sep 17 00:00:00 2001 From: Jordan Luiz Ribeiro Duarte Date: Wed, 1 Jul 2026 23:19:30 -0400 Subject: [PATCH] feat(buildagents): read CI_MERGE_REQUEST_REF_PATH in GitLabCi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In GitLab MR pipelines, CI_COMMIT_REF_NAME is the source branch name, not the merge request ref. Resolve the current branch with precedence: CI_COMMIT_TAG → CI_MERGE_REQUEST_REF_PATH → CI_COMMIT_REF_NAME. Keeps GitLab CI logic in GitVersion.BuildAgents only (no Core changes), following the same pass-through pattern as Azure Pipelines and GitHub Actions. Add unit and integration tests for the MR workflow and document the required pull-request regex for GitLab's merge-requests/ namespace. --- .../docs/reference/build-servers/gitlab.md | 31 ++++++++++++ .../PullRequestInBuildAgentTest.cs | 35 ++++++++++++-- .../Agents/GitLabCiTests.cs | 47 ++++++++++++++----- src/GitVersion.BuildAgents/Agents/GitLabCi.cs | 21 +++++++-- 4 files changed, 113 insertions(+), 21 deletions(-) diff --git a/docs/input/docs/reference/build-servers/gitlab.md b/docs/input/docs/reference/build-servers/gitlab.md index aa97d9e5fd..541efe4e73 100755 --- a/docs/input/docs/reference/build-servers/gitlab.md +++ b/docs/input/docs/reference/build-servers/gitlab.md @@ -9,6 +9,37 @@ To use GitVersion with GitLab CI, either use the [MSBuild Task](/docs/usage/msbuild) or put the GitVersion executable in your runner's `PATH`. +### Merge Request pipelines + +In merge request pipelines GitLab sets `CI_MERGE_REQUEST_REF_PATH` (for example +`refs/merge-requests/15/head` or `refs/merge-requests/15/merge`). GitVersion +reads this variable through the `GitLabCi` build agent when it is present, +following the same pass-through pattern as Azure Pipelines (`BUILD_SOURCEBRANCH`) +and GitHub Actions (`GITHUB_REF`). + +Branch resolution order in `GitLabCi`: + +1. `CI_COMMIT_TAG` set — treat as a tag pipeline (`GetCurrentBranch` returns `null`) +2. `CI_MERGE_REQUEST_REF_PATH` set — use the merge request ref +3. otherwise — `CI_COMMIT_REF_NAME` (branch name) + +After repository normalisation the friendly branch name becomes +`merge-requests//head` or `merge-requests//merge`. Extend the +`pull-request` branch configuration in `GitVersion.yml` so the regex matches +GitLab's namespace (the default `pull-requests|pull|pr` pattern does not): + +```yaml +workflow: GitFlow/v1 +branches: + pull-request: + regex: ^merge-requests/(?\d+)/(head|merge)$ + label: PullRequest{Number} +``` + +`CI_COMMIT_REF_NAME` still contains the source branch name (for example +`feature/foo`) in MR pipelines; it is ignored when `CI_MERGE_REQUEST_REF_PATH` +is set. + A working example of integrating GitVersion with GitLab is maintained in the project [Utterly Automated Versioning][utterly-automated-versioning] Here is a summary of what it demonstrated (many more details in the [Readme][readme]) diff --git a/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs b/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs index 6530705ac5..408d50dc65 100644 --- a/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs +++ b/src/GitVersion.App.Tests/PullRequestInBuildAgentTest.cs @@ -22,6 +22,12 @@ public class PullRequestInBuildAgentTest "refs/remotes/pull-requests/5/merge" ]; + private static readonly string[] GitLabMergeRequestRefs = + [ + "refs/merge-requests/5/head", + "refs/merge-requests/5/merge" + ]; + [TestCaseSource(nameof(PrMergeRefs))] public async Task VerifyAzurePipelinesPullRequest(string pullRequestRef) { @@ -75,15 +81,16 @@ public async Task VerifyGitHubActionsPullRequest(string pullRequestRef) await VerifyPullRequestVersionIsCalculatedProperly(pullRequestRef, env); } - [TestCaseSource(nameof(PrMergeRefs))] - public async Task VerifyGitLabCIPullRequest(string pullRequestRef) + [TestCaseSource(nameof(GitLabMergeRequestRefs))] + public async Task VerifyGitLabCIPullRequest(string mergeRequestRef) { var env = new Dictionary { { GitLabCi.EnvironmentVariableName, "true" }, - { "CI_COMMIT_REF_NAME", PullRequestBranchName } + { GitLabCi.MergeRequestRefPathEnvironmentVariableName, mergeRequestRef }, + { GitLabCi.CommitRefNameEnvironmentVariableName, "FeatureBranch" } }; - await VerifyPullRequestVersionIsCalculatedProperly(pullRequestRef, env); + await VerifyGitLabMergeRequestVersionIsCalculatedProperly(mergeRequestRef, env); } [TestCaseSource(nameof(PrMergeRefs))] @@ -142,9 +149,29 @@ public async Task VerifyBitBucketPipelinesPullRequest(string pullRequestRef) await VerifyPullRequestVersionIsCalculatedProperly(pullRequestRef, env); } + private const string GitLabMergeRequestPullRequestConfig = """ + workflow: GitFlow/v1 + branches: + pull-request: + regex: ^merge-requests/(?\d+)/(head|merge)$ + """; + + private static async Task VerifyGitLabMergeRequestVersionIsCalculatedProperly(string mergeRequestRef, Dictionary env) + { + using var fixture = new EmptyRepositoryFixture(); + var configPath = FileSystemHelper.Path.Combine(fixture.RepositoryPath, "GitVersion.yml"); + await File.WriteAllTextAsync(configPath, GitLabMergeRequestPullRequestConfig); + await VerifyPullRequestVersionIsCalculatedProperly(fixture, mergeRequestRef, env); + } + private static async Task VerifyPullRequestVersionIsCalculatedProperly(string pullRequestRef, Dictionary env) { using var fixture = new EmptyRepositoryFixture(); + await VerifyPullRequestVersionIsCalculatedProperly(fixture, pullRequestRef, env); + } + + private static async Task VerifyPullRequestVersionIsCalculatedProperly(EmptyRepositoryFixture fixture, string pullRequestRef, Dictionary env) + { var remoteRepositoryPath = FileSystemHelper.Path.GetRepositoryTempPath(); RepositoryFixtureBase.Init(remoteRepositoryPath); using var remoteRepository = new Repository(remoteRepositoryPath); diff --git a/src/GitVersion.BuildAgents.Tests/Agents/GitLabCiTests.cs b/src/GitVersion.BuildAgents.Tests/Agents/GitLabCiTests.cs index fa6c190587..b99429b370 100644 --- a/src/GitVersion.BuildAgents.Tests/Agents/GitLabCiTests.cs +++ b/src/GitVersion.BuildAgents.Tests/Agents/GitLabCiTests.cs @@ -27,7 +27,13 @@ public void SetUp() } [TearDown] - public void TearDown() => this.environment.SetEnvironmentVariable(GitLabCi.EnvironmentVariableName, null); + public void TearDown() + { + this.environment.SetEnvironmentVariable(GitLabCi.EnvironmentVariableName, null); + this.environment.SetEnvironmentVariable(GitLabCi.CommitRefNameEnvironmentVariableName, null); + this.environment.SetEnvironmentVariable(GitLabCi.CommitTagEnvironmentVariableName, null); + this.environment.SetEnvironmentVariable(GitLabCi.MergeRequestRefPathEnvironmentVariableName, null); + } [Test] public void ShouldSetBuildNumber() @@ -51,7 +57,7 @@ public void ShouldSetOutputVariables() [TestCase("#3-change_projectname", "#3-change_projectname")] public void GetCurrentBranchShouldHandleBranches(string branchName, string expectedResult) { - this.environment.SetEnvironmentVariable("CI_COMMIT_REF_NAME", branchName); + this.environment.SetEnvironmentVariable(GitLabCi.CommitRefNameEnvironmentVariableName, branchName); var result = this.buildServer.GetCurrentBranch(false); @@ -64,8 +70,8 @@ public void GetCurrentBranchShouldHandleBranches(string branchName, string expec [TestCase("v1.2.1", "v1.2.1", null)] public void GetCurrentBranchShouldHandleTags(string branchName, string commitTag, string? expectedResult) { - this.environment.SetEnvironmentVariable("CI_COMMIT_REF_NAME", branchName); - this.environment.SetEnvironmentVariable("CI_COMMIT_TAG", commitTag); // only set in pipelines for tags + this.environment.SetEnvironmentVariable(GitLabCi.CommitRefNameEnvironmentVariableName, branchName); + this.environment.SetEnvironmentVariable(GitLabCi.CommitTagEnvironmentVariableName, commitTag); // only set in pipelines for tags var result = this.buildServer.GetCurrentBranch(false); @@ -79,18 +85,33 @@ public void GetCurrentBranchShouldHandleTags(string branchName, string commitTag } } - [TestCase("main", "main")] - [TestCase("dev", "dev")] - [TestCase("development", "development")] - [TestCase("my_cool_feature", "my_cool_feature")] - [TestCase("#3-change_projectname", "#3-change_projectname")] - public void GetCurrentBranchShouldHandlePullRequests(string branchName, string expectedResult) + [TestCase("refs/merge-requests/1/head", "refs/merge-requests/1/head")] + [TestCase("refs/merge-requests/42/merge", "refs/merge-requests/42/merge")] + public void GetCurrentBranchShouldHandleMergeRequestRefPaths(string refPath, string expected) { - this.environment.SetEnvironmentVariable("CI_COMMIT_REF_NAME", branchName); + this.environment.SetEnvironmentVariable(GitLabCi.MergeRequestRefPathEnvironmentVariableName, refPath); + this.environment.SetEnvironmentVariable(GitLabCi.CommitRefNameEnvironmentVariableName, "developer"); - var result = this.buildServer.GetCurrentBranch(false); + this.buildServer.GetCurrentBranch(false).ShouldBe(expected); + } - result.ShouldBe(expectedResult); + [Test] + public void GetCurrentBranchShouldPreferTagOverMergeRequestRefPath() + { + this.environment.SetEnvironmentVariable(GitLabCi.MergeRequestRefPathEnvironmentVariableName, "refs/merge-requests/1/head"); + this.environment.SetEnvironmentVariable(GitLabCi.CommitRefNameEnvironmentVariableName, "v1.0.0"); + this.environment.SetEnvironmentVariable(GitLabCi.CommitTagEnvironmentVariableName, "v1.0.0"); + + this.buildServer.GetCurrentBranch(false).ShouldBeNull(); + } + + [Test] + public void GetCurrentBranchShouldFallBackToRefNameWhenMergeRequestRefPathIsEmpty() + { + this.environment.SetEnvironmentVariable(GitLabCi.MergeRequestRefPathEnvironmentVariableName, ""); + this.environment.SetEnvironmentVariable(GitLabCi.CommitRefNameEnvironmentVariableName, "developer"); + + this.buildServer.GetCurrentBranch(false).ShouldBe("developer"); } [Test] diff --git a/src/GitVersion.BuildAgents/Agents/GitLabCi.cs b/src/GitVersion.BuildAgents/Agents/GitLabCi.cs index c6c3bae5ed..1d06a4ea23 100644 --- a/src/GitVersion.BuildAgents/Agents/GitLabCi.cs +++ b/src/GitVersion.BuildAgents/Agents/GitLabCi.cs @@ -7,6 +7,9 @@ namespace GitVersion.Agents; internal class GitLabCi : BuildAgentBase { public const string EnvironmentVariableName = "GITLAB_CI"; + public const string CommitRefNameEnvironmentVariableName = "CI_COMMIT_REF_NAME"; + public const string CommitTagEnvironmentVariableName = "CI_COMMIT_TAG"; + public const string MergeRequestRefPathEnvironmentVariableName = "CI_MERGE_REQUEST_REF_PATH"; private string? file; public GitLabCi(IEnvironment environment, ILog log, IFileSystem fileSystem) : base(environment, log, fileSystem) => WithPropertyFile("gitversion.properties"); @@ -22,14 +25,24 @@ public override string[] SetOutputVariables(string name, string? value) => $"GitVersion_{name}={value}" ]; + // CI_MERGE_REQUEST_REF_PATH is only available in merge request pipelines, // CI_COMMIT_REF_NAME can contain either the branch or the tag // See https://docs.gitlab.com/ee/ci/variables/predefined_variables.html // CI_COMMIT_TAG is only available in tag pipelines, // so we can exit if CI_COMMIT_REF_NAME would return the tag - public override string? GetCurrentBranch(bool usingDynamicRepos) => - string.IsNullOrEmpty(this.environment.GetEnvironmentVariable("CI_COMMIT_TAG")) - ? this.environment.GetEnvironmentVariable("CI_COMMIT_REF_NAME") - : null; + public override string? GetCurrentBranch(bool usingDynamicRepos) + { + if (!string.IsNullOrEmpty(this.environment.GetEnvironmentVariable(CommitTagEnvironmentVariableName))) + { + return null; + } + var mergeRequestRefPath = this.environment.GetEnvironmentVariable(MergeRequestRefPathEnvironmentVariableName); + if (!string.IsNullOrEmpty(mergeRequestRefPath)) + { + return mergeRequestRefPath; + } + return this.environment.GetEnvironmentVariable(CommitRefNameEnvironmentVariableName); + } public override bool PreventFetch() => true;