Skip to content

Commit 32b4f73

Browse files
robertcoltheartarturcic
authored andcommitted
fix: throw on missing properties and use integer fallbacks
1 parent 9c18202 commit 32b4f73

8 files changed

Lines changed: 111 additions & 64 deletions

File tree

BREAKING_CHANGES.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
## v6.8.0
2-
* When using `{env:VAR_NAME}` or `{BranchName}` syntax in labels or version formatting, a missing environment variable or placeholder no longer throws and instead returns a blank value.
3-
41
## v6.2.0
52

63
* The configuration property `label-number-pattern` was removed. The functionality can be still used by changing the label and the branch name regular expression for pull-request branches.

docs/input/docs/reference/mdsource/configuration.source.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ of `alpha.foo` with `label: 'alpha.{BranchName}'` and `regex: '^features?[\/-](?
504504

505505
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+)[-/].+'`.
506506

507-
You can also use environment variable placeholders with the `{env:VARIABLE_NAME}` syntax. Environment variable placeholders can also be combined with regex placeholders, for example `{BranchName}-{env:VARIABLE_NAME}`, and support fallback values using the `{env:VARIABLE_NAME ?? "fallback"}` syntax. These can all be combined with cascading fallbacks using environment variables and placeholders like this: `{env:VARIABLE_NAME ?? BranchName ?? "fallback"}`. If no value is found and no fallback is provided, the placeholder expands to an empty string.
507+
You can also use environment variable placeholders with the `{env:VARIABLE_NAME}` syntax. Environment variable placeholders can also be combined with regex placeholders, for example `{BranchName}-{env:VARIABLE_NAME}`, and support fallback values using the `{env:VARIABLE_NAME ?? "fallback"}` syntax. These can be combined with cascading fallbacks using environment variables and placeholders like this: `{env:VARIABLE_NAME ?? BranchName ?? "fallback"}`.
508508

509509
**Note:** To clear a default use an empty string: `label: ''`
510510

src/GitVersion.Configuration.Tests/Configuration/ConfigurationExtensionsTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void EnsureGetBranchSpecificLabelWorksWithoutEnvironmentWhenNoEnvPlacehol
127127
}
128128

129129
[Test]
130-
public void EnsureGetBranchSpecificLabelReturnsEmptyWhenEnvVarMissing()
130+
public void EnsureGetBranchSpecificLabelThrowsWhenEnvVarMissing()
131131
{
132132
var environment = new TestEnvironment();
133133
// Do not set MISSING_VAR
@@ -140,7 +140,7 @@ public void EnsureGetBranchSpecificLabelReturnsEmptyWhenEnvVarMissing()
140140
.Build();
141141

142142
var effectiveConfiguration = configuration.GetEffectiveConfiguration(ReferenceName.FromBranchName(BranchName));
143-
var actual = effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName(BranchName), null, environment);
144-
actual.ShouldBe("pr-");
143+
Should.Throw<ArgumentException>(() =>
144+
effectiveConfiguration.GetBranchSpecificLabel(ReferenceName.FromBranchName(BranchName), null, environment));
145145
}
146146
}

src/GitVersion.Core.Tests/Extensions/StringFormatWithExtensionTests.cs

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public void FormatWithEnvVarTokenWithFallback()
5656
{
5757
this.environment.SetEnvironmentVariable("GIT_VERSION_TEST_VAR", "Env Var Value");
5858
var propertyObject = new { };
59-
const string target = "{env:GIT_VERSION_TEST_VAR ?? fallback}";
59+
const string target = "{env:GIT_VERSION_TEST_VAR ?? \"fallback\"}";
6060
const string expected = "Env Var Value";
6161
var actual = target.FormatWith(propertyObject, this.environment);
6262
Assert.That(actual, Is.EqualTo(expected));
@@ -67,7 +67,7 @@ public void FormatWithUnsetEnvVarToken_WithFallback()
6767
{
6868
this.environment.SetEnvironmentVariable("GIT_VERSION_UNSET_TEST_VAR", null);
6969
var propertyObject = new { };
70-
const string target = "{env:GIT_VERSION_UNSET_TEST_VAR ?? fallback}";
70+
const string target = "{env:GIT_VERSION_UNSET_TEST_VAR ?? \"fallback\"}";
7171
const string expected = "fallback";
7272
var actual = target.FormatWith(propertyObject, this.environment);
7373
Assert.That(actual, Is.EqualTo(expected));
@@ -79,9 +79,7 @@ public void FormatWithUnsetEnvVarToken_WithoutFallback()
7979
this.environment.SetEnvironmentVariable("GIT_VERSION_UNSET_TEST_VAR", null);
8080
var propertyObject = new { };
8181
const string target = "{env:GIT_VERSION_UNSET_TEST_VAR}";
82-
const string expected = "";
83-
var actual = target.FormatWith(propertyObject, this.environment);
84-
Assert.That(actual, Is.EqualTo(expected));
82+
Assert.Throws<ArgumentException>(() => target.FormatWith(propertyObject, this.environment));
8583
}
8684

8785
[Test]
@@ -100,17 +98,15 @@ public void FormatWithMultipleEnvVars()
10098
public void FormatWithMultipleEnvChars()
10199
{
102100
var propertyObject = new { };
103-
const string target = "{env:env:GIT_VERSION_TEST_VAR_1} and {env:DUMMY_VAR ?? fallback}";
104-
const string expected = " and fallback";
105-
var actual = target.FormatWith(propertyObject, this.environment);
106-
Assert.That(actual, Is.EqualTo(expected));
101+
const string target = "{env:env:GIT_VERSION_TEST_VAR_1} and {env:DUMMY_VAR ?? \"fallback\"}";
102+
Assert.Throws<ArgumentException>(() => target.FormatWith(propertyObject, this.environment));
107103
}
108104

109105
[Test]
110106
public void FormatWithMultipleFallbackChars()
111107
{
112108
var propertyObject = new { };
113-
const string target = " and {env:DUMMY_VAR ??? fallback}";
109+
const string target = " and {env:DUMMY_VAR ??? \"fallback\"}";
114110
Assert.Throws<FormatException>(() => target.FormatWith(propertyObject, this.environment));
115111
}
116112

@@ -119,17 +115,15 @@ public void FormatWithSingleFallbackChar()
119115
{
120116
this.environment.SetEnvironmentVariable("DUMMY_ENV_VAR", "DummyVal");
121117
var propertyObject = new { };
122-
const string target = "{en:DUMMY_ENV_VAR} and {env:DUMMY_ENV_VAR??fallback}";
123-
const string expected = " and DummyVal";
124-
var actual = target.FormatWith(propertyObject, this.environment);
125-
Assert.That(actual, Is.EqualTo(expected));
118+
const string target = "{en:DUMMY_ENV_VAR} and {env:DUMMY_ENV_VAR??\"fallback\"}";
119+
Assert.Throws<ArgumentException>(() => target.FormatWith(propertyObject, this.environment));
126120
}
127121

128122
[Test]
129123
public void FormatWithNullPropagationWithMultipleSpaces()
130124
{
131125
var propertyObject = new { SomeProperty = "Some Value" };
132-
const string target = "{SomeProperty} and {env:DUMMY_ENV_VAR ?? fallback}";
126+
const string target = "{SomeProperty} and {env:DUMMY_ENV_VAR ?? \"fallback\"}";
133127
const string expected = "Some Value and fallback";
134128
var actual = target.FormatWith(propertyObject, this.environment);
135129
Assert.That(actual, Is.EqualTo(expected));
@@ -157,13 +151,11 @@ public void FormatWithMissingEnvAndPropertyFallback()
157151
}
158152

159153
[Test]
160-
public void FormatWithMultiplePropertiesAndFallback()
154+
public void FormatWithMultiplePropertiesAndNoFallback()
161155
{
162156
var propertyObject = new { };
163-
const string target = "{SomeProperty ?? SomeOtherProperty ?? MissingPropTreatedAsLiteral}";
164-
const string expected = "MissingPropTreatedAsLiteral";
165-
var actual = target.FormatWith(propertyObject, this.environment);
166-
Assert.That(actual, Is.EqualTo(expected));
157+
const string target = "{SomeProperty ?? SomeOtherProperty ?? MissingProp}";
158+
Assert.Throws<ArgumentException>(() => target.FormatWith(propertyObject, this.environment));
167159
}
168160

169161
[Test]
@@ -180,12 +172,22 @@ public void FormatWithMultiplePropertiesAndQuotedFallback()
180172
public void FormatWithMultipleAvailablePropertiesAndFallback()
181173
{
182174
var propertyObject = new { SomeOtherProperty = "Some-Value" };
183-
const string target = "{SomeProperty ?? SomeOtherProperty ?? fallback}";
175+
const string target = "{SomeProperty ?? SomeOtherProperty ?? \"fallback\"}";
184176
const string expected = "Some-Value";
185177
var actual = target.FormatWith(propertyObject, this.environment);
186178
Assert.That(actual, Is.EqualTo(expected));
187179
}
188180

181+
[Test]
182+
public void FormatWithMissingPropertiesAndIntegerFallback()
183+
{
184+
var propertyObject = new { };
185+
const string target = "{SomeProperty ?? SomeOtherProperty ?? 47}";
186+
const string expected = "47";
187+
var actual = target.FormatWith(propertyObject, this.environment);
188+
Assert.That(actual, Is.EqualTo(expected));
189+
}
190+
189191
[Test]
190192
public void FormatWithPropertyAndEnvAndFormatters()
191193
{
@@ -197,6 +199,38 @@ public void FormatWithPropertyAndEnvAndFormatters()
197199
Assert.That(actual, Is.EqualTo(expected));
198200
}
199201

202+
[TestCase("{env:VARIABLE ?? \"\"}", null, null, "")]
203+
[TestCase("{env:MISSING ?? env:VARIABLE}", "Var", null, "Var")]
204+
[TestCase("{Property ?? \"\"}", null, null, "")]
205+
[TestCase("{Property ?? 47}", null, null, "47")]
206+
[TestCase("{Property ?? env:VARIABLE}", null, "Branch", "Branch")]
207+
[TestCase("{Property ?? env:VARIABLE ?? \"\"}", null, null, "")]
208+
[TestCase("{Property ?? env:VARIABLE ?? 42}", null, null, "42")]
209+
public void FormatWith_EnvVarAndPropertyAndFallback_DoesNotThrow(string input, string? envVar, string? property, string expected)
210+
{
211+
if (envVar != null)
212+
{
213+
this.environment.SetEnvironmentVariable("VARIABLE", envVar);
214+
}
215+
216+
object propertyObject = property != null
217+
? new { Property = property }
218+
: new { };
219+
220+
var actual = input.FormatWith(propertyObject, this.environment);
221+
Assert.That(actual, Is.EqualTo(expected));
222+
}
223+
224+
[TestCase("{env:VARIABLE}")]
225+
[TestCase("{Property}")]
226+
[TestCase("{Property ?? env:VARIABLE}")]
227+
[TestCase("{Property ?? Property}")]
228+
public void FormatWith_MissingEnvVarOrPropertyAndNoFallback_Throws(string input)
229+
{
230+
object propertyObject = new { };
231+
Assert.Throws<ArgumentException>(() => input.FormatWith(propertyObject, this.environment));
232+
}
233+
200234
[Test]
201235
public void FormatEnvVar_WithFallback_QuotedAndEmpty()
202236
{
@@ -247,7 +281,7 @@ public void FormatProperty_NullInteger()
247281
public void FormatProperty_String_WithFallback()
248282
{
249283
var propertyObject = new { Property = "Value" };
250-
const string target = "{Property ?? fallback}";
284+
const string target = "{Property ?? \"fallback\"}";
251285
var actual = target.FormatWith(propertyObject, this.environment);
252286
Assert.That(actual, Is.EqualTo("Value"));
253287
}
@@ -256,7 +290,7 @@ public void FormatProperty_String_WithFallback()
256290
public void FormatProperty_Integer_WithFallback()
257291
{
258292
var propertyObject = new { Property = 42 };
259-
const string target = "{Property ?? fallback}";
293+
const string target = "{Property ?? \"fallback\"}";
260294
var actual = target.FormatWith(propertyObject, this.environment);
261295
Assert.That(actual, Is.EqualTo("42"));
262296
}
@@ -265,7 +299,7 @@ public void FormatProperty_Integer_WithFallback()
265299
public void FormatProperty_NullObject_WithFallback()
266300
{
267301
var propertyObject = new { Property = (object?)null };
268-
const string target = "{Property ?? fallback}";
302+
const string target = "{Property ?? \"fallback\"}";
269303
var actual = target.FormatWith(propertyObject, this.environment);
270304
Assert.That(actual, Is.EqualTo("fallback"));
271305
}
@@ -274,9 +308,9 @@ public void FormatProperty_NullObject_WithFallback()
274308
public void FormatProperty_NullInteger_WithFallback()
275309
{
276310
var propertyObject = new { Property = (int?)null };
277-
const string target = "{Property ?? fallback}";
311+
const string target = "{Property ?? 43}";
278312
var actual = target.FormatWith(propertyObject, this.environment);
279-
Assert.That(actual, Is.EqualTo("fallback"));
313+
Assert.That(actual, Is.EqualTo("43"));
280314
}
281315

282316
[Test]

src/GitVersion.Core.Tests/Formatting/LabelTokenizerTests.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ public class LabelTokenizerTests
2424
[TestCase("Pat\"tern")]
2525
public void ParseTokens_InvalidLiterals_Throws(string input) => AssertThrows(input);
2626

27-
[TestCase("Prop ?? literal", "Prop", "literal")]
28-
[TestCase("Prop??literal", "Prop", "literal")]
29-
[TestCase("Prop ?? literal ?? fallback", "Prop", "literal", "fallback")]
30-
[TestCase("Prop ??literal?? fallback", "Prop", "literal", "fallback")]
31-
[TestCase("Prop ?? \"literal\" ?? fallback", "Prop", "literal", "fallback")]
32-
[TestCase("Prop:format ?? \"literal\" ?? fallback", "Prop", "literal", "fallback")]
33-
[TestCase("env:Prop ?? \"literal\" ?? fallback", "Prop", "literal", "fallback")]
34-
[TestCase("env:Prop:format ?? \"literal\" ?? fallback", "Prop", "literal", "fallback")]
27+
[TestCase("Prop1 ?? Prop2", "Prop1", "Prop2")]
28+
[TestCase("Prop1??Prop2", "Prop1", "Prop2")]
29+
[TestCase("Prop1??Prop2??42", "Prop1", "Prop2", "42")]
30+
[TestCase("Prop1??Prop2??\"42\"", "Prop1", "Prop2", "42")]
31+
[TestCase("Prop1 ?? Prop2 ?? \"fallback\"", "Prop1", "Prop2", "fallback")]
32+
[TestCase("Prop1 ??Prop2?? \"fallback\"", "Prop1", "Prop2", "fallback")]
33+
[TestCase("Prop1 ?? Prop2 ?? 42", "Prop1", "Prop2", "42")]
34+
[TestCase("Prop1:format ?? Prop2 ?? \"fallback\"", "Prop1", "Prop2", "fallback")]
35+
[TestCase("env:Env1 ?? Prop2 ?? \"fallback\"", "Env1", "Prop2", "fallback")]
36+
[TestCase("env:Env1:format ?? \"literal\" ?? \"fallback\"", "Env1", "literal", "fallback")]
37+
[TestCase("env:Env1 ?? 42", "Env1", "42")]
3538
public void ParseTokens_ValidIdentifiers_ReturnsValid(string input, params string[] expected) => AssertTokens(input, expected);
3639

3740
[TestCase("Prop ??? literal")]

src/GitVersion.Core/Formatting/LabelTokenizer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ private static LabelToken ParseToken(string identifier)
5252
return new LabelToken(identifier[1..^1], LabelTokenType.Literal);
5353
}
5454

55+
if (int.TryParse(identifier, out _))
56+
{
57+
return new LabelToken(identifier, LabelTokenType.Literal);
58+
}
59+
5560
var (propertyName, propertyFormat) = ParseKeyAndFormat(identifier);
5661

5762
return new LabelToken(propertyName, LabelTokenType.Property, propertyFormat);

src/GitVersion.Core/Formatting/MemberResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public MemberInfo[] ResolveMemberPath(Type type, string memberExpression)
1515
{
1616
var recursivePath = FindMemberRecursive(type, memberName, []);
1717
return recursivePath == null
18-
? []
18+
? throw new ArgumentException($"'{memberName}' is not a property or field on type '{type.Name}'")
1919
: [.. recursivePath];
2020
}
2121

src/GitVersion.Core/Formatting/StringFormatWithExtension.cs

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -84,38 +84,45 @@ private static string EvaluateMatch(string input, Func<string, string?> memberEv
8484
var tokenizer = new LabelTokenizer(input);
8585
var tokens = tokenizer.ParseTokens().ToArray();
8686

87+
Exception? lastException = null;
88+
8789
foreach (var token in tokens)
8890
{
8991
if (token.Type == LabelTokenType.Literal)
9092
{
9193
return token.Name;
9294
}
9395

94-
var value = token.Type == LabelTokenType.Environment
95-
? environment.GetEnvironmentVariable(InputSanitizer.SanitizeEnvVarName(token.Name))
96-
: memberEvaluator(token.Name);
97-
98-
if (value is not null && !string.IsNullOrEmpty(token.Format))
96+
try
9997
{
100-
if (ValueFormatter.Default.TryFormat(value, InputSanitizer.SanitizeFormat(token.Format), out var formatted))
98+
var value = token.Type == LabelTokenType.Environment
99+
? EvaluateEnvVar(token.Name, environment)
100+
: memberEvaluator(token.Name);
101+
102+
if (value is not null && !string.IsNullOrEmpty(token.Format))
101103
{
102-
return formatted;
104+
if (ValueFormatter.Default.TryFormat(value, InputSanitizer.SanitizeFormat(token.Format), out var formatted))
105+
{
106+
return formatted;
107+
}
108+
109+
return value;
103110
}
104111

105-
return value;
112+
if (value is not null)
113+
{
114+
return value;
115+
}
106116
}
107-
108-
if (value is not null)
117+
catch (Exception e)
109118
{
110-
return value;
119+
lastException = e;
111120
}
112121
}
113122

114-
var lastToken = tokens.LastOrDefault();
115-
116-
if (tokens.Length > 1 && lastToken?.Type == LabelTokenType.Property)
123+
if (lastException != null)
117124
{
118-
return lastToken.Name;
125+
throw lastException;
119126
}
120127

121128
return string.Empty;
@@ -125,12 +132,6 @@ private static string EvaluateMatch(string input, Func<string, string?> memberEv
125132
{
126133
var safeMember = InputSanitizer.SanitizeMemberName(member);
127134
var memberPath = MemberResolver.ResolveMemberPath(source.GetType(), safeMember);
128-
129-
if (memberPath.Length == 0)
130-
{
131-
return null;
132-
}
133-
134135
var getter = ExpressionCompiler.CompileGetter(source.GetType(), memberPath);
135136

136137
var value = getter(source);
@@ -143,8 +144,15 @@ private static string EvaluateMatch(string input, Func<string, string?> memberEv
143144
var safeMember = InputSanitizer.SanitizeMemberName(member);
144145

145146
if (!source.TryGetValue(safeMember, out var value))
146-
return null;
147+
throw new ArgumentException($"'{safeMember}' is not a valid placeholder");
147148

148149
return value?.ToString();
149150
}
151+
152+
private static string EvaluateEnvVar(string name, IEnvironment environment)
153+
{
154+
var safeName = InputSanitizer.SanitizeEnvVarName(name);
155+
156+
return environment.GetEnvironmentVariable(name) ?? throw new ArgumentException($"Environment variable {safeName} not found and no fallback provided");
157+
}
150158
}

0 commit comments

Comments
 (0)