diff --git a/docs/input/docs/reference/configuration.md b/docs/input/docs/reference/configuration.md index 9c4dd1a8f6..0b0f470999 100644 --- a/docs/input/docs/reference/configuration.md +++ b/docs/input/docs/reference/configuration.md @@ -892,6 +892,32 @@ of `alpha.foo` with `label: 'alpha.{BranchName}'` and `regex: '^features?[\/-](? Another example: branch `features/sc-12345/some-description` would become a pre-release label of `sc-12345` with `label: '{StoryNo}'` and `regex: '^features?[\/-](?sc-\d+)[-/].+'`. +You can also use environment variable placeholders with the `{env:VARIABLE_NAME}` syntax. This is particularly useful for CI/CD scenarios, such as using GitHub Actions context variables in pull request labels. + +For example, to use the GitHub Actions head ref in a pull request label: +```yaml +branches: + pull-request: + label: 'pr-{env:GITHUB_HEAD_REF}' + regex: '^pull[/-]' +``` + +You can combine regex placeholders with environment variables: +```yaml +branches: + feature: + label: '{BranchName}-{env:GITHUB_HEAD_REF}' + regex: '^features?[\/-](?.+)' +``` + +Environment variables support fallback values using the `{env:VARIABLE_NAME ?? "fallback"}` syntax: +```yaml +branches: + pull-request: + label: 'pr-{env:GITHUB_HEAD_REF ?? "unknown"}' + regex: '^pull[/-]' +``` + **Note:** To clear a default use an empty string: `label: ''` ### increment diff --git a/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs b/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs index 97e225f752..0b90186a54 100644 --- a/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs +++ b/src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs @@ -7,6 +7,7 @@ namespace GitVersion.Core.Tests.Configuration; [TestFixture] public class ConfigurationExtensionsTests : TestBase { + private const string PullRequestBranch = "pull-request"; [TestCase("release/2.0.0", "refs/heads/release/2.0.0", "release/2.0.0", "release/2.0.0", true, false, false, false, true)] @@ -55,4 +56,94 @@ public void EnsureGetBranchSpecificLabelWorksAsExpected(string branchName, strin var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName(branchName), null); actual.ShouldBe(expectedLabel); } + + [Test] + public void EnsureGetBranchSpecificLabelProcessesEnvironmentVariables() + { + var environment = new TestEnvironment(); + environment.SetEnvironmentVariable("GITHUB_HEAD_REF", "feature-branch"); + + var configuration = GitFlowConfigurationBuilder.New + .WithoutBranches() + .WithBranch(PullRequestBranch, builder => builder + .WithLabel("pr-{env:GITHUB_HEAD_REF}") + .WithRegularExpression(@"^pull[/-]")) + .Build(); + + var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName(PullRequestBranch)); + var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName(PullRequestBranch), null, environment); + actual.ShouldBe("pr-feature-branch"); + } + + [Test] + public void EnsureGetBranchSpecificLabelProcessesEnvironmentVariablesWithFallback() + { + var environment = new TestEnvironment(); + // Don't set GITHUB_HEAD_REF to test fallback + + var configuration = GitFlowConfigurationBuilder.New + .WithoutBranches() + .WithBranch(PullRequestBranch, builder => builder + .WithLabel("pr-{env:GITHUB_HEAD_REF ?? \"unknown\"}") + .WithRegularExpression(@"^pull[/-]")) + .Build(); + + var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName(PullRequestBranch)); + var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName(PullRequestBranch), null, environment); + actual.ShouldBe("pr-unknown"); + } + + [Test] + public void EnsureGetBranchSpecificLabelProcessesEnvironmentVariablesAndRegexPlaceholders() + { + var environment = new TestEnvironment(); + environment.SetEnvironmentVariable("GITHUB_HEAD_REF", "feature-branch"); + + var configuration = GitFlowConfigurationBuilder.New + .WithoutBranches() + .WithBranch("feature/test-branch", builder => builder + .WithLabel("{BranchName}-{env:GITHUB_HEAD_REF}") + .WithRegularExpression(@"^features?[\/-](?.+)")) + .Build(); + + var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName("feature/test-branch")); + var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName("feature/test-branch"), null, environment); + actual.ShouldBe("test-branch-feature-branch"); + } + + [Test] + public void EnsureGetBranchSpecificLabelWorksWithoutEnvironmentWhenNoEnvPlaceholders() + { + var configuration = GitFlowConfigurationBuilder.New + .WithoutBranches() + .WithBranch("feature/test", builder => builder + .WithLabel("{BranchName}") + .WithRegularExpression(@"^features?[\/-](?.+)")) + .Build(); + + var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName("feature/test")); + var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName("feature/test"), null, null); + actual.ShouldBe("test"); + } + + [Test] + public void EnsureGetBranchSpecificLabelProcessesEnvironmentVariablesEvenWhenRegexDoesNotMatch() + { + var environment = new TestEnvironment(); + environment.SetEnvironmentVariable("GITHUB_HEAD_REF", "feature-branch"); + + var configuration = GitFlowConfigurationBuilder.New + .WithoutBranches() + .WithBranch(PullRequestBranch, builder => builder + .WithLabel("pr-{env:GITHUB_HEAD_REF}") + .WithRegularExpression(@"^pull[/-]")) + .WithBranch("feature", builder => builder + .WithLabel("{BranchName}") + .WithRegularExpression(@"^features?[\/-](?.+)")) + .Build(); + + var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName("feature/other-branch")); + var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName("feature/other-branch"), null, environment); + actual.ShouldBe("other-branch"); + } } diff --git a/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs b/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs index 3830bf8962..4267236530 100644 --- a/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs +++ b/src/GitVersion.Core/Extensions/ConfigurationExtensions.cs @@ -1,5 +1,6 @@ using GitVersion.Core; using GitVersion.Extensions; +using GitVersion.Formatting; using GitVersion.Git; using GitVersion.VersionCalculation; @@ -7,6 +8,10 @@ namespace GitVersion.Configuration; internal static class ConfigurationExtensions { + // Empty object used as source parameter when only processing environment variables in FormatWith + // FormatWith requires a non-null source, but we don't need any properties from it when only using env: placeholders + private static readonly object EmptyFormatSource = new { }; + extension(IGitVersionConfiguration configuration) { public EffectiveBranchConfiguration GetEffectiveBranchConfiguration(IBranch branch, EffectiveConfiguration? parentConfiguration = null) @@ -97,10 +102,10 @@ private static bool ShouldBeIgnored(ICommit commit, IIgnoreConfiguration ignore) extension(EffectiveConfiguration configuration) { - public string? GetBranchSpecificLabel(ReferenceName branchName, string? branchNameOverride) - => GetBranchSpecificLabel(configuration, branchName.WithoutOrigin, branchNameOverride); + public string? GetBranchSpecificLabel(ReferenceName branchName, string? branchNameOverride, IEnvironment? environment = null) + => GetBranchSpecificLabel(configuration, branchName.WithoutOrigin, branchNameOverride, environment); - public string? GetBranchSpecificLabel(string? branchName, string? branchNameOverride) + public string? GetBranchSpecificLabel(string? branchName, string? branchNameOverride, IEnvironment? environment = null) { configuration.NotNull(); @@ -111,10 +116,43 @@ private static bool ShouldBeIgnored(ICommit commit, IIgnoreConfiguration ignore) } var effectiveBranchName = branchNameOverride ?? branchName; - if (configuration.RegularExpression.IsNullOrWhiteSpace() || effectiveBranchName.IsNullOrEmpty()) return label; + if (configuration.RegularExpression.IsNullOrWhiteSpace() || effectiveBranchName.IsNullOrEmpty()) + { + // Even if regex doesn't match, we should still process environment variables + if (environment is not null) + { + try + { + label = label.FormatWith(new { }, environment); + } + catch (ArgumentException) + { + // If environment variable is missing and no fallback, return label as-is + // This maintains backward compatibility + } + } + return label; + } + var regex = RegexPatterns.Cache.GetOrAdd(configuration.RegularExpression); var match = regex.Match(effectiveBranchName); - if (!match.Success) return label; + if (!match.Success) + { + // Even if regex doesn't match, we should still process environment variables + if (environment is not null) + { + try + { + label = label.FormatWith(new { }, environment); + } + catch (ArgumentException) + { + // If environment variable is missing and no fallback, return label as-is + } + } + return label; + } + foreach (var groupName in regex.GetGroupNames()) { var groupValue = match.Groups[groupName].Value; @@ -128,6 +166,21 @@ private static bool ShouldBeIgnored(ICommit commit, IIgnoreConfiguration ignore) startIndex = index + escapedGroupValue.Length; } } + + // Process environment variable placeholders after regex placeholders + if (environment is not null) + { + try + { + label = label.FormatWith(new { }, environment); + } + catch (ArgumentException) + { + // If environment variable is missing and no fallback, return label as-is + // This maintains backward compatibility + } + } + return label; } diff --git a/src/GitVersion.Core/VersionCalculation/VersionCalculators/NextVersionCalculator.cs b/src/GitVersion.Core/VersionCalculation/VersionCalculators/NextVersionCalculator.cs index fc38d26801..37cb2558cf 100644 --- a/src/GitVersion.Core/VersionCalculation/VersionCalculators/NextVersionCalculator.cs +++ b/src/GitVersion.Core/VersionCalculation/VersionCalculators/NextVersionCalculator.cs @@ -14,13 +14,15 @@ internal class NextVersionCalculator( IEnumerable deploymentModeCalculators, IEnumerable versionStrategies, IEffectiveBranchConfigurationFinder effectiveBranchConfigurationFinder, - ITaggedSemanticVersionService taggedSemanticVersionService) + ITaggedSemanticVersionService taggedSemanticVersionService, + IEnvironment environment) : INextVersionCalculator { private readonly ILog log = log.NotNull(); private readonly Lazy versionContext = versionContext.NotNull(); private readonly IVersionStrategy[] versionStrategies = [.. versionStrategies.NotNull()]; private readonly IEffectiveBranchConfigurationFinder effectiveBranchConfigurationFinder = effectiveBranchConfigurationFinder.NotNull(); + private readonly IEnvironment environment = environment.NotNull(); private GitVersionContext Context => this.versionContext.Value; @@ -108,7 +110,7 @@ private bool TryGetSemanticVersion( { result = null; - var label = effectiveConfiguration.GetBranchSpecificLabel(Context.CurrentBranch.Name, null); + var label = effectiveConfiguration.GetBranchSpecificLabel(Context.CurrentBranch.Name, null, this.environment); var currentCommitTaggedVersion = taggedSemanticVersionsOfCurrentCommit .Where(element => element.Value.IsMatchForBranchSpecificLabel(label)).Max();