From 4d23b7ad7ce427802b69f1c34faf13c2ac8892be Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sun, 3 Dec 2023 15:37:56 +0100 Subject: [PATCH] azp: fix variable availability (#275) * azp: fix variable availability * add some tests and known issue --- src/Sdk/AzurePipelines/AzureDevops.cs | 357 ++++++++++++++---- src/Sdk/AzurePipelines/azurepiplines.json | 30 ++ .../pipeline.yml | 45 +++ .../pipeline.yml | 58 +++ 4 files changed, 426 insertions(+), 64 deletions(-) create mode 100644 testworkflows/azpipelines/dynamic-variable-interpolation-legacy/pipeline.yml create mode 100644 testworkflows/azpipelines/dynamic-variable-interpolation/pipeline.yml diff --git a/src/Sdk/AzurePipelines/AzureDevops.cs b/src/Sdk/AzurePipelines/AzureDevops.cs index 5280ba8ba04..8a11a74dce1 100644 --- a/src/Sdk/AzurePipelines/AzureDevops.cs +++ b/src/Sdk/AzurePipelines/AzureDevops.cs @@ -55,17 +55,81 @@ public static TemplateContext CreateTemplateContext(GitHub.DistributedTask.Objec return templateContext; } - public static async Task ParseVariables(Runner.Server.Azure.Devops.Context context, IDictionary vars, TemplateToken rawvars, bool onlyStaticVars = false) { - if(rawvars is MappingToken mvars) { - foreach(var kv in mvars) { - // Skip expressions if we parse static variables - if(onlyStaticVars && (kv.Key is ExpressionToken || kv.Value is ExpressionToken)) { + public static async Task ParseVariables(Runner.Server.Azure.Devops.Context context, IDictionary vars, TemplateToken rawvars, TemplateContext staticVarCtx = null) { + DictionaryContextData staticVars = null; + if(staticVarCtx != null && staticVarCtx.ExpressionValues.TryGetValue("variables", out var rvars) && rvars is GitHub.DistributedTask.Expressions2.Sdk.IReadOnlyObject tvars) { + staticVars = new DictionaryContextData(); + foreach (var k in tvars.Keys) { + var v = tvars[k] as GitHub.DistributedTask.Expressions2.Sdk.IString; + staticVars[k] = new StringContextData(v.GetString()); + } + staticVarCtx.ExpressionValues["variables"] = staticVars; + } + if(rawvars is MappingToken mvars) + { + var map = new MappingToken(rawvars.FileId, rawvars.Line, rawvars.Column); + foreach(var x in ProcessVariableMapping(vars, staticVarCtx, staticVars, mvars)) { + map.Add(x); + } + return map; + } + else if(rawvars is SequenceToken svars) + { + var seq = new SequenceToken(rawvars.FileId, rawvars.Line, rawvars.Column); + await foreach(var x in ProcessVariableSequence(context, vars, staticVarCtx, staticVars, svars)) { + seq.Add(x); + } + return seq; + } + return null; + + static IEnumerable> ProcessVariableMapping(IDictionary vars, TemplateContext staticVarCtx, DictionaryContextData staticVars, MappingToken mvars) + { + for (int i = 0; i < mvars.Count; i++) + { + var kv = mvars[i]; + // Eval expressions if we parse static variables + if (staticVarCtx != null && (kv.Key is ExpressionToken || kv.Value is ExpressionToken)) + { + // Need to group Expressions in keys as necessary + // Don't group independent if, elseif, else blocks + var evT = new MappingToken(kv.Key.FileId, kv.Key.Line, kv.Key.Column) { kv }; + if (kv.Key.Type == TokenType.IfExpression) + { + while ((i + 1) < mvars.Count && (mvars[i + 1].Key.Type & (TokenType.ElseIfExpression | TokenType.ElseExpression)) != 0) + { + i++; + evT.Add(mvars[i]); + if (mvars[i].Key.Type == TokenType.ElseExpression) + { + break; + } + } + } + var res = TemplateEvaluator.Evaluate(staticVarCtx, kv.Key is ExpressionToken ? "single-layer-workflow-mapping" : "workflow-value", evT, 0, kv.Key.FileId); + if (res is MappingToken r) + { + foreach(var l in ProcessVariableMapping(vars, staticVarCtx, staticVars, r)) { + yield return l; + } + } continue; } - vars[kv.Key.AssertString("variables").Value] = kv.Value.AssertLiteralString("variables"); + var k = kv.Key.AssertString("variables").Value; + var v = kv.Value.AssertLiteralString("variables"); + vars[k] = v; + if(staticVars != null) { + staticVars[k] = new StringContextData(v); + } + yield return kv; } - } else { - foreach(var rawdef in rawvars.AssertSequence("")) { + } + + static async IAsyncEnumerable ProcessVariableSequence(Context context, IDictionary vars, TemplateContext staticVarCtx, DictionaryContextData staticVars, SequenceToken svars) + { + for (int i = 0; i < svars.Count; i++) + { + var rawdef = svars[i]; string template = null; TemplateToken parameters = null; string name = null; @@ -73,53 +137,113 @@ public static async Task ParseVariables(Runner.Server.Azure.Devops.Context conte bool isReadonly = false; string group = null; bool skip = false; - foreach(var kv in rawdef.AssertMapping("")) { - if(onlyStaticVars && (kv.Key is ExpressionToken || kv.Value is ExpressionToken)) { + if (staticVars != null && rawdef is ExpressionToken) + { + var s = new SequenceToken(rawdef.FileId, rawdef.Line, rawdef.Column) { rawdef }; + var res = TemplateEvaluator.Evaluate(staticVarCtx, "workflow-value", s, 0, rawdef.FileId); + if(res is SequenceToken st) { + await foreach(var t in ProcessVariableSequence(context, vars, staticVarCtx, staticVars, st)) { + yield return t; + } + } + continue; + } + var vdef = rawdef.AssertMapping(""); + if (vdef.Count == 1 && vdef[0].Key.Type == TokenType.IfExpression && vdef[0].Value.Type == TokenType.Sequence) + { + var s = new SequenceToken(rawdef.FileId, rawdef.Line, rawdef.Column) { vdef }; + while (i + 1 < svars.Count && svars[i + 1] is MappingToken mdef && mdef.Count == 1 && (mdef[0].Key.Type & (TokenType.ElseIfExpression | TokenType.ElseExpression)) != 0 && mdef[0].Value.Type == TokenType.Sequence) + { + i++; + s.Add(svars[i]); + if ((svars[i] as MappingToken)[0].Key.Type == TokenType.ElseExpression) + { + break; + } + } + var res = TemplateEvaluator.Evaluate(staticVarCtx, "single-layer-workflow-sequence", s, 0, rawdef.FileId); + if(res is SequenceToken st) { + await foreach(var t in ProcessVariableSequence(context, vars, staticVarCtx, staticVars, st)) { + yield return t; + } + } + continue; + } + foreach (var kv in vdef) + { + if (staticVars != null && (kv.Key is ExpressionToken || kv.Value is ExpressionToken)) + { skip = true; break; } var primaryKey = kv.Key.AssertString("variables").Value; - switch(primaryKey) { + switch (primaryKey) + { case "template": template = kv.Value.AssertString("variables").Value; - break; + break; case "parameters": parameters = kv.Value; - break; + break; case "name": name = kv.Value.AssertLiteralString("variables"); - break; + break; case "value": value = kv.Value.AssertLiteralString("variables"); - break; + break; case "readonly": isReadonly = kv.Value.AssertAzurePipelinesBoolean("variables"); - break; + break; case "group": group = kv.Value.AssertLiteralString("variables"); - break; + break; } } // Skip expressions and template references if we parse static variables - if(skip || onlyStaticVars && template != null) { + if (skip || staticVars != null && template != null) + { + if (skip) + { + var s = new SequenceToken(rawdef.FileId, rawdef.Line, rawdef.Column) { rawdef }; + var res = TemplateEvaluator.Evaluate(staticVarCtx, "workflow-value", s, 0, rawdef.FileId); + if(res is SequenceToken st) { + await foreach(var t in ProcessVariableSequence(context, vars, staticVarCtx, staticVars, st)) { + yield return t; + } + } + } else { + yield return rawdef; + } continue; } - if(group != null) { - // Skip metainfo while preprocessing via onlyStaticVars - if(!onlyStaticVars) { + yield return rawdef; + if (group != null) + { + // Skip metainfo while preprocessing via staticVars != null + if (staticVars == null) + { vars[Guid.NewGuid().ToString()] = new VariableValue(group) { IsGroup = true }; } var groupVars = context.VariablesProvider?.GetVariablesForEnvironment(group); - if(groupVars != null) { - foreach(var v in groupVars) { + if (groupVars != null) + { + foreach (var v in groupVars) + { vars[v.Key] = new VariableValue(v.Value) { IsGroupMember = true }; } } - } else if(template != null) { + } + else if (template != null) + { var file = await ReadTemplate(context, template, parameters != null ? parameters.AssertMapping("param").ToDictionary(kv => kv.Key.AssertString("").Value, kv => kv.Value) : null, "variable-template-root"); await ParseVariables(context.ChildContext(file, template), vars, (from e in file where e.Key.AssertString("").Value == "variables" select e.Value).First()); - } else { + } + else + { vars[name] = new VariableValue(value, isReadonly: isReadonly); + if(staticVars != null) { + staticVars[name] = new StringContextData(value); + } } } } @@ -459,19 +583,22 @@ public static string RelativeTo(string cwd, string filename) { return string.Join('/', path.ToArray()); } - public static async Task ReadTemplate(Runner.Server.Azure.Devops.Context context, string filenameAndRef, Dictionary cparameters = null, string schemaName = null) { + public static async Task ReadTemplate(Runner.Server.Azure.Devops.Context context, string filenameAndRef, Dictionary cparameters = null, string schemaName = null) + { var variables = context.VariablesProvider?.GetVariablesForEnvironment(""); var afilenameAndRef = filenameAndRef.Split("@", 2); var filename = afilenameAndRef[0]; // Read the file var finalRepository = afilenameAndRef.Length == 1 ? context.RepositoryAndRef : string.Equals(afilenameAndRef[1], "self", StringComparison.OrdinalIgnoreCase) ? null : (context.Repositories?.TryGetValue(afilenameAndRef[1], out var ralias) ?? false) ? ralias : throw new Exception($"Couldn't find repository with alias {afilenameAndRef[1]} in repository resources"); var finalFileName = RelativeTo(context.RepositoryAndRef == finalRepository ? context.CWD ?? "." : "/", filename); - if(finalFileName == null) { + if (finalFileName == null) + { throw new Exception($"Couldn't find template location {filenameAndRef}"); } var fileContent = await context.FileProvider.ReadFile(finalRepository, finalFileName); - if(fileContent == null) { + if (fileContent == null) + { throw new Exception($"Couldn't read template {filenameAndRef} resolved to {finalFileName} ({finalRepository ?? "self"})"); } context.TraceWriter?.Info("{0}", $"Parsing template {filenameAndRef} resolved to {finalFileName} ({finalRepository ?? "self"}) using Schema {schemaName ?? "pipeline-root"}"); @@ -497,17 +624,20 @@ public static async Task ReadTemplate(Runner.Server.Azure.Devops.C TemplateToken parameters = null; TemplateToken rawStaticVariables = null; - foreach(var kv in pipelineroot) { - if(kv.Key.Type != TokenType.String) { + foreach (var kv in pipelineroot) + { + if (kv.Key.Type != TokenType.String) + { continue; } - switch((kv.Key as StringToken)?.Value) { + switch ((kv.Key as StringToken)?.Value) + { case "parameters": - parameters = kv.Value; - break; + parameters = kv.Value; + break; case "variables": - rawStaticVariables = kv.Value; - break; + rawStaticVariables = kv.Value; + break; } } @@ -516,60 +646,78 @@ public static async Task ReadTemplate(Runner.Server.Azure.Devops.C contextData["parameters"] = parametersData; var variablesData = new DictionaryContextData(); contextData["variables"] = variablesData; - if(variables != null) { - foreach(var v in variables) { + if (variables != null) + { + foreach (var v in variables) + { variablesData[v.Key] = new StringContextData(v.Value); } } - - if(parameters?.Type == TokenType.Mapping) { + + if (parameters?.Type == TokenType.Mapping) + { int providedParameter = 0; - foreach(var kv in parameters as MappingToken) { - if(kv.Key.Type != TokenType.String) { + foreach (var kv in parameters as MappingToken) + { + if (kv.Key.Type != TokenType.String) + { continue; } var paramname = (kv.Key as StringToken)?.Value; - if(cparameters?.TryGetValue(paramname, out var value) == true) { + if (cparameters?.TryGetValue(paramname, out var value) == true) + { parametersData[paramname] = value.ToContextData(); providedParameter++; - } else { + } + else + { parametersData[paramname] = kv.Value.ToContextData(); } } - if(cparameters != null && providedParameter != cparameters?.Count) { + if (cparameters != null && providedParameter != cparameters?.Count) + { throw new Exception("Provided undeclared parameters"); } - } else if(parameters is SequenceToken sparameters) { - foreach(var mparam in sparameters) { + } + else if (parameters is SequenceToken sparameters) + { + foreach (var mparam in sparameters) + { var varm = mparam.AssertMapping("varm"); string name = null; string type = "string"; TemplateToken def = null; TemplateToken values = null; - foreach(var kv in varm) { - switch((kv.Key as StringToken).Value) { + foreach (var kv in varm) + { + switch ((kv.Key as StringToken).Value) + { case "name": name = kv.Value.AssertLiteralString("name"); - break; + break; case "type": type = kv.Value.AssertLiteralString("type"); - break; + break; case "default": def = kv.Value; - break; + break; case "values": values = kv.Value; - break; + break; } } - if (name == null) { + if (name == null) + { templateContext.Error(sparameters, "A value for the 'name' parameter must be provided."); continue; } var defCtxData = def == null ? null : await ConvertValue(context, def, type, values); - if(cparameters?.TryGetValue(name, out var value) == true || def == null && (value = await (context.RequiredParametersProvider?.GetRequiredParameter(name) ?? Task.FromResult(null))) != null) { + if (cparameters?.TryGetValue(name, out var value) == true || def == null && (value = await (context.RequiredParametersProvider?.GetRequiredParameter(name) ?? Task.FromResult(null))) != null) + { parametersData[name] = await ConvertValue(context, value, type, values); - } else { + } + else + { if (def == null) // handle missing required parameter { templateContext.Error(new TemplateValidationError($"A value for the '{name}' parameter must be provided.")); @@ -577,15 +725,19 @@ public static async Task ReadTemplate(Runner.Server.Azure.Devops.C parametersData[name] = defCtxData; } } - } else { - if(cparameters != null && 0 != cparameters.Count) { + } + else + { + if (cparameters != null && 0 != cparameters.Count) + { throw new Exception("Provided undeclared parameters"); } } if (cparameters != null) { - foreach(var unexpectedParameter in cparameters.Keys.Where(i => !parametersData.ContainsKey(i))) { + foreach (var unexpectedParameter in cparameters.Keys.Where(i => !parametersData.ContainsKey(i))) + { templateContext.Error(parameters, $"Unexpected parameter '{unexpectedParameter}'"); } } @@ -593,24 +745,33 @@ public static async Task ReadTemplate(Runner.Server.Azure.Devops.C templateContext.Errors.Check(); var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach(var param in parametersData) { + foreach (var param in parametersData) + { dict[param.Key] = param.Value; } - if(rawStaticVariables != null) { + if (rawStaticVariables != null) + { // See "testworkflows/azpipelines/expressions-docs/Conditionally assign a variable.yml" templateContext = AzureDevops.CreateTemplateContext(context.TraceWriter ?? new EmptyTraceWriter(), templateContext.GetFileTable().ToArray(), context.Flags, contextData); templateContext.ExpressionValues["parameters"] = new ParametersContextData(dict, templateContext.Errors); - rawStaticVariables = TemplateEvaluator.Evaluate(templateContext, "workflow-value", rawStaticVariables, 0, fileId); - templateContext.Errors.Check(); + // rawStaticVariables = TemplateEvaluator.Evaluate(templateContext, "workflow-value", rawStaticVariables, 0, fileId); + // templateContext.Errors.Check(); IDictionary pvars = new Dictionary(StringComparer.OrdinalIgnoreCase); - await ParseVariables(context, pvars, rawStaticVariables, true); - foreach(var v in pvars) { + pipelineroot[pipelineroot.Select((x, i) => (x, i)).First(x => x.x.Key.ToString() == "variables").i] = new KeyValuePair(new StringToken(null, null, null, "variables"), await ParseVariables(context, pvars, rawStaticVariables, templateContext)); + foreach (var v in pvars) + { variablesData[v.Key] = new StringContextData(v.Value.Value); } } + //pipelineroot.Traverse().Any(t => t is ExpressionToken); + // lookahead vars + // TODO Generalize and stepwise patch the template structure + templateContext = await evalJobsWithExtraVars(context, templateContext, fileId, contextData, variablesData, dict, pipelineroot, null); + templateContext = await evalStagesWithExtraVars(context, templateContext, fileId, pipelineroot, contextData, variablesData, dict); + templateContext = AzureDevops.CreateTemplateContext(context.TraceWriter ?? new EmptyTraceWriter(), templateContext.GetFileTable().ToArray(), context.Flags, contextData); templateContext.ExpressionValues["parameters"] = new ParametersContextData(dict, templateContext.Errors); @@ -618,6 +779,74 @@ public static async Task ReadTemplate(Runner.Server.Azure.Devops.C templateContext.Errors.Check(); context.TraceWriter?.Verbose("{0}", evaluatedResult.ToContextData().ToJToken().ToString()); return evaluatedResult.AssertMapping("root"); + + static async Task evalJobsWithExtraVars(Context context, TemplateContext templateContext, int fileId, DictionaryContextData contextData, DictionaryContextData variablesData, Dictionary dict, GitHub.DistributedTask.Expressions2.Sdk.IReadOnlyObject stage, DictionaryContextData vardata) + { + if (stage.TryGetValue("jobs", out var rjobs) && rjobs is SequenceToken jobs) + { + foreach (var ji in jobs.ToArray().Select((t, i) => new { t, i })) + { + var rjob = ji.t; + if (rjob is GitHub.DistributedTask.Expressions2.Sdk.IReadOnlyObject job && job.TryGetValue("variables", out var rjvars) && rjvars is TemplateToken jvars) + { + templateContext = AzureDevops.CreateTemplateContext(context.TraceWriter ?? new EmptyTraceWriter(), templateContext.GetFileTable().ToArray(), context.Flags, contextData); + templateContext.ExpressionValues["parameters"] = new ParametersContextData(dict, templateContext.Errors); + if(vardata != null) { + templateContext.ExpressionValues["variables"] = vardata; + } + IDictionary pjvars = new Dictionary(StringComparer.OrdinalIgnoreCase); + jvars = await ParseVariables(context, pjvars, jvars, templateContext); + var varjdata = (vardata ?? variablesData).Clone() as DictionaryContextData; + foreach (var v in pjvars) + { + varjdata[v.Key] = new StringContextData(v.Value.Value); + } + templateContext = AzureDevops.CreateTemplateContext(context.TraceWriter ?? new EmptyTraceWriter(), templateContext.GetFileTable().ToArray(), context.Flags, contextData); + templateContext.ExpressionValues["parameters"] = new ParametersContextData(dict, templateContext.Errors); + templateContext.ExpressionValues["variables"] = varjdata; + (rjob as MappingToken)[(rjob as MappingToken).Select((x, i) => (x, i)).First(x => x.x.Key.ToString() == "variables").i] = new KeyValuePair(new StringToken(null, null, null, "variables"), jvars); + rjob = TemplateEvaluator.Evaluate(templateContext, "workflow-value", rjob, 0, fileId); + templateContext.Errors.Check(); + jobs[ji.i] = rjob; + } + } + } + + return templateContext; + } + + static async Task evalStagesWithExtraVars(Context context, TemplateContext templateContext, int fileId, MappingToken pipelineroot, DictionaryContextData contextData, DictionaryContextData variablesData, Dictionary dict) + { + if ((pipelineroot as GitHub.DistributedTask.Expressions2.Sdk.IReadOnlyObject).TryGetValue("stages", out var rstages) && rstages is SequenceToken stages) + { + foreach (var si in stages.ToArray().Select((t, i) => new { t, i })) + { + var rstage = si.t; + if (rstage is GitHub.DistributedTask.Expressions2.Sdk.IReadOnlyObject stage && stage.TryGetValue("variables", out var rvars) && rvars is TemplateToken vars) + { + templateContext = AzureDevops.CreateTemplateContext(context.TraceWriter ?? new EmptyTraceWriter(), templateContext.GetFileTable().ToArray(), context.Flags, contextData); + templateContext.ExpressionValues["parameters"] = new ParametersContextData(dict, templateContext.Errors); + IDictionary pvars = new Dictionary(StringComparer.OrdinalIgnoreCase); + vars = await ParseVariables(context, pvars, vars, templateContext); + var vardata = variablesData.Clone() as DictionaryContextData; + foreach (var v in pvars) + { + vardata[v.Key] = new StringContextData(v.Value.Value); + } + templateContext = await evalJobsWithExtraVars(context, templateContext, fileId, contextData, variablesData, dict, stage, vardata); + templateContext = AzureDevops.CreateTemplateContext(context.TraceWriter ?? new EmptyTraceWriter(), templateContext.GetFileTable().ToArray(), context.Flags, contextData); + templateContext.ExpressionValues["parameters"] = new ParametersContextData(dict, templateContext.Errors); + templateContext.ExpressionValues["variables"] = vardata; + (rstage as MappingToken)[(rstage as MappingToken).Select((x, i) => (x, i)).First(x => x.x.Key.ToString() == "variables").i] = new KeyValuePair(new StringToken(null, null, null, "variables"), vars); + rstage = TemplateEvaluator.Evaluate(templateContext, "workflow-value", rstage, 0, fileId); + templateContext.Errors.Check(); + stages[si.i] = rstage; + } + } + } + + return templateContext; + } } public static TemplateToken ConvertAllScalarsToString(TemplateToken token) { diff --git a/src/Sdk/AzurePipelines/azurepiplines.json b/src/Sdk/AzurePipelines/azurepiplines.json index a0c173400ef..cf9ce44e682 100644 --- a/src/Sdk/AzurePipelines/azurepiplines.json +++ b/src/Sdk/AzurePipelines/azurepiplines.json @@ -32,6 +32,36 @@ ] }, + "workflow-value-no-expand": { + "context": [ + "no-expand" + ], + "one-of": [ + "boolean", "mapping", "null", "number", "sequence", "string" + ] + }, + + "single-layer-workflow-mapping": { + "context": [ + "parameters", + "variables" + ], + "mapping": { + "loose-key-type": "workflow-key", + "loose-value-type": "workflow-value-no-expand" + } + }, + + "single-layer-workflow-sequence": { + "context": [ + "parameters", + "variables" + ], + "sequence": { + "item-type": "workflow-value-no-expand" + } + }, + "pipeline-container": { "description": "Pipeline container as of https://learn.microsoft.com/en-us/azure/devops/release-notes/2022/pipelines/sprint-212-update", "mapping": { diff --git a/testworkflows/azpipelines/dynamic-variable-interpolation-legacy/pipeline.yml b/testworkflows/azpipelines/dynamic-variable-interpolation-legacy/pipeline.yml new file mode 100644 index 00000000000..34ce4c8a113 --- /dev/null +++ b/testworkflows/azpipelines/dynamic-variable-interpolation-legacy/pipeline.yml @@ -0,0 +1,45 @@ +parameters: +- name: test + type: boolean + default: true +stages: +- stage: + variables: + nothing: test + othervar2: ${{ variables.requiredVar }} + jobs: + - job: + variables: + ${{ if not(parameters.test) }}: + requiredVar: Substitute-test-s + othervar10: ${{ variables.requiredVar }} + ${{ else }}: + requiredVar: Substitute-test-s3 + othervar10: ${{ variables.requiredVar }} + ${{ if variables.requiredVar }}: + requiredVar2: ${{ variables.requiredVar }}-s2 + othervar11: ${{ variables.requiredVar2 }} + othervar4: ${{ variables.othervar }} + othervar: ${{ variables.requiredVar }} + othervar3: ${{ variables.othervar }} + # TODO fix bug + # chained-0: loop-0 + # ${{ each x in split('0,1|1,2|2,3', '|') }}: + # chained-${{ split(x, ',')[1] }}: ${{ variables[format('chained-{0}', split(x, ',')[0])] }}-${{ split(x, ',')[1] }} + steps: + - ${{ if ne(variables.requiredVar, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar10, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.requiredVar2, 'Substitute-test-s3-s2') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar11, 'Substitute-test-s3-s2') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar4, '') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar3, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar2, '') }}: + - assert: ${{ converttojson(variables) }} \ No newline at end of file diff --git a/testworkflows/azpipelines/dynamic-variable-interpolation/pipeline.yml b/testworkflows/azpipelines/dynamic-variable-interpolation/pipeline.yml new file mode 100644 index 00000000000..919ff00dab5 --- /dev/null +++ b/testworkflows/azpipelines/dynamic-variable-interpolation/pipeline.yml @@ -0,0 +1,58 @@ +parameters: +- name: test + type: boolean + default: true +stages: +- stage: + variables: + nothing: test + othervar2: ${{ variables.requiredVar }} + jobs: + - job: + variables: + - ${{ if not(parameters.test) }}: + - name: requiredVar + value: Substitute-test-s + - name: othervar10 + value: ${{ variables.requiredVar }} + - ${{ else }}: + - name: requiredVar + value: Substitute-test-s3 + - name: othervar10 + value: ${{ variables.requiredVar }} + - ${{ if variables.requiredVar }}: + - name: requiredVar2 + value: ${{ variables.requiredVar }}-s2 + - name: othervar11 + value: ${{ variables.requiredVar2 }} + - name: othervar4 + value: ${{ variables.othervar }} + - name: othervar + value: ${{ variables.requiredVar }} + - name: othervar3 + value: ${{ variables.othervar }} + # TODO fix bug + # - name: chained-0 + # value: loop-0 + # - ${{ each x in split('0,1|1,2|2,3', '|')}}: + # - name: chained-${{ split(x, ',')[1] }} + # value: ${{ variables[format('chained-{0}', split(x, ',')[0])] }}-${{ split(x, ',')[1] }} + steps: + - ${{ if ne(variables.requiredVar, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar10, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.requiredVar2, 'Substitute-test-s3-s2') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar11, 'Substitute-test-s3-s2') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar4, '') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar3, 'Substitute-test-s3') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar2, '') }}: + - assert: ${{ converttojson(variables) }} + - ${{ if ne(variables.othervar2, '') }}: + - assert: ${{ converttojson(variables) }} \ No newline at end of file