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; diff --git a/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs b/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs index 2c6b2fef35..e9998b1de5 100644 --- a/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs +++ b/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs @@ -143,4 +143,24 @@ public void EnsureGetBranchSpecificLabelThrowsWhenEnvVarMissing() Should.Throw(() => effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName(BranchName), null, environment)); } + + [TestCase("case-00/my-branch", "case-00-my-branch")] + [TestCase("my-branch", "my-branch")] + [TestCase("my_branch/valid", "my-branch-valid")] + public void EnsureGetBranchSpecificLabelReturnsValidLabelForEnvironmentVariables(string variable, string expectedLabel) + { + var environment = new TestEnvironment(); + environment.SetEnvironmentVariable("GITHUB_HEAD_REF", variable); + + var configuration = GitFlowConfigurationBuilder.New + .WithoutBranches() + .WithBranch("feature/test-feature", builder => builder + .WithLabel("{env:GITHUB_HEAD_REF}") + .WithRegularExpression(@"^features?[\/-](?.+)")) + .Build(); + + var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName("feature/test-feature")); + var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName("feature/test-feature"), null, environment); + actual.ShouldBe(expectedLabel); + } } diff --git a/src/GitVersion.Core/Core/RegexPatterns.cs b/src/GitVersion.Core/Core/RegexPatterns.cs index 79dc6da59f..f01b6e603e 100644 --- a/src/GitVersion.Core/Core/RegexPatterns.cs +++ b/src/GitVersion.Core/Core/RegexPatterns.cs @@ -34,6 +34,9 @@ internal static partial class RegexPatterns [StringSyntax(StringSyntaxAttribute.Regex, Options)] internal const string SanitizeNameRegexPattern = "[^a-zA-Z0-9-]"; + [StringSyntax(StringSyntaxAttribute.Regex, Options)] + internal const string SanitizeLabelRegexPattern = "[^a-zA-Z0-9-.]"; + #if NET9_0_OR_GREATER [GeneratedRegex(SwitchArgumentRegexPattern, Options)] public static partial Regex SwitchArgumentRegex { get; } diff --git a/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs b/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs index 97ffb4c88e..3f4ac9a428 100644 --- a/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs +++ b/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs @@ -133,7 +133,8 @@ private static bool ShouldBeIgnored(ICommit commit, IIgnoreConfiguration ignore) var effectiveBranchName = branchNameOverride ?? branchName; var labelPlaceholders = BuildLabelPlaceholders(configuration.RegularExpression, effectiveBranchName); - return label.FormatWith(labelPlaceholders, environment); + return label.FormatWith(labelPlaceholders, environment) + .RegexReplace(RegexPatterns.SanitizeLabelRegexPattern, "-"); } public TaggedSemanticVersions GetTaggedSemanticVersion()