Skip to content
26 changes: 26 additions & 0 deletions docs/input/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?[\/-](?<StoryNo>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?[\/-](?<BranchName>.+)'
```

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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?[\/-](?<BranchName>.+)"))
.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?[\/-](?<BranchName>.+)"))
.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?[\/-](?<BranchName>.+)"))
.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");
}
}
63 changes: 58 additions & 5 deletions src/GitVersion.Core/Extensions/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using GitVersion.Core;
using GitVersion.Extensions;
using GitVersion.Formatting;
using GitVersion.Git;
using GitVersion.VersionCalculation;

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)
Expand Down Expand Up @@ -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();

Expand All @@ -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;
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ internal class NextVersionCalculator(
IEnumerable<IDeploymentModeCalculator> deploymentModeCalculators,
IEnumerable<IVersionStrategy> versionStrategies,
IEffectiveBranchConfigurationFinder effectiveBranchConfigurationFinder,
ITaggedSemanticVersionService taggedSemanticVersionService)
ITaggedSemanticVersionService taggedSemanticVersionService,
IEnvironment environment)
: INextVersionCalculator
{
private readonly ILog log = log.NotNull();
private readonly Lazy<GitVersionContext> 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;

Expand Down Expand Up @@ -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();

Expand Down
Loading