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
@@ -1,4 +1,5 @@
using GitVersion.Configuration;
using GitVersion.Core;

Check warning on line 2 in src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / DotNet Format

Using directive is unnecessary.

Check warning on line 2 in src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / DotNet Format

Using directive is unnecessary.

Check warning on line 2 in src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / DotNet Format

Using directive is unnecessary.

Check warning on line 2 in src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / DotNet Format

Using directive is unnecessary.
using GitVersion.Core.Tests.Helpers;
using GitVersion.Git;

Expand Down Expand Up @@ -55,4 +56,91 @@
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("pull-request", builder => builder
.WithLabel("pr-{env:GITHUB_HEAD_REF}")
.WithRegularExpression(@"^pull[/-]"))
.Build();

var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName("pull-request"));
var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName("pull-request"), 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("pull-request", builder => builder
.WithLabel("pr-{env:GITHUB_HEAD_REF ?? \"unknown\"}")
.WithRegularExpression(@"^pull[/-]"))
.Build();

var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName("pull-request"));
var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName("pull-request"), 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("pull-request", builder => builder
.WithLabel("pr-{env:GITHUB_HEAD_REF}")
.WithRegularExpression(@"^pull[/-]"))
.Build();

var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName("other-branch"));
var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName("other-branch"), null, environment);
actual.ShouldBe("pr-feature-branch");
}
}
59 changes: 54 additions & 5 deletions src/GitVersion.Core/Extensions/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.IO.Abstractions;
using GitVersion.Core;
using GitVersion.Extensions;
using GitVersion.Formatting;
using GitVersion.Git;
using GitVersion.Helpers;
using GitVersion.VersionCalculation;
Expand Down Expand Up @@ -75,11 +76,11 @@ public static bool IsReleaseBranch(this IGitVersionConfiguration configuration,
=> configuration.GetBranchConfiguration(branchName).IsReleaseBranch ?? false;

public static string? GetBranchSpecificLabel(
this EffectiveConfiguration configuration, ReferenceName branchName, string? branchNameOverride)
=> GetBranchSpecificLabel(configuration, branchName.WithoutOrigin, branchNameOverride);
this EffectiveConfiguration configuration, ReferenceName branchName, string? branchNameOverride, IEnvironment? environment = null)
=> GetBranchSpecificLabel(configuration, branchName.WithoutOrigin, branchNameOverride, environment);

public static string? GetBranchSpecificLabel(
this EffectiveConfiguration configuration, string? branchName, string? branchNameOverride)
this EffectiveConfiguration configuration, string? branchName, string? branchNameOverride, IEnvironment? environment = null)
{
configuration.NotNull();

Expand All @@ -90,10 +91,43 @@ public static bool IsReleaseBranch(this IGitVersionConfiguration configuration,
}

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 @@ -107,6 +141,21 @@ public static bool IsReleaseBranch(this IGitVersionConfiguration configuration,
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