github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/tekton/syntax/pipeline_test.go (about)

     1  // +build unit
     2  
     3  package syntax_test
     4  
     5  import (
     6  	"context"
     7  	"errors"
     8  	"path/filepath"
     9  	"testing"
    10  
    11  	v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    12  	"github.com/olli-ai/jx/v2/pkg/versionstream"
    13  	"github.com/stretchr/testify/assert"
    14  	"k8s.io/apimachinery/pkg/api/resource"
    15  
    16  	"github.com/google/go-cmp/cmp"
    17  	"github.com/olli-ai/jx/v2/pkg/config"
    18  	"github.com/olli-ai/jx/v2/pkg/tekton/syntax"
    19  	sh "github.com/olli-ai/jx/v2/pkg/tekton/syntax/syntax_helpers_test"
    20  	tektonv1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
    21  	tb "github.com/tektoncd/pipeline/test/builder"
    22  	corev1 "k8s.io/api/core/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	kubefake "k8s.io/client-go/kubernetes/fake"
    25  	"knative.dev/pkg/apis"
    26  	"knative.dev/pkg/kmp"
    27  )
    28  
    29  var (
    30  	// Needed to take address of strings since workspace is *string. Is there a better way to handle optional values?
    31  	defaultWorkspace = "default"
    32  	customWorkspace  = "custom"
    33  )
    34  
    35  // TODO: Try to write some helper functions to make Pipeline and Task expect building less bloody verbose.
    36  func TestParseJenkinsfileYaml(t *testing.T) {
    37  	testVersionsDir := filepath.Join("test_data", "stable_versions")
    38  	resolvedGitMergeImage, err := versionstream.ResolveDockerImage(testVersionsDir, syntax.GitMergeImage)
    39  	assert.NoError(t, err)
    40  
    41  	ctx := context.Background()
    42  	tests := []struct {
    43  		name               string
    44  		expected           *syntax.ParsedPipeline
    45  		pipeline           *tektonv1alpha1.Pipeline
    46  		tasks              []*tektonv1alpha1.Task
    47  		expectedErrorMsg   string
    48  		validationErrorMsg string
    49  		structure          *v1.PipelineStructure
    50  	}{
    51  		{
    52  			name: "simple_jenkinsfile",
    53  			expected: sh.ParsedPipeline(
    54  				sh.PipelineAgent("some-image"),
    55  				sh.PipelineStage("A Working Stage",
    56  					sh.StageStep(
    57  						sh.StepCmd("echo"),
    58  						sh.StepArg("hello"), sh.StepArg("world"),
    59  						sh.StepName("A Step With Spaces And Such"),
    60  					),
    61  				),
    62  			),
    63  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
    64  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
    65  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
    66  				),
    67  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
    68  			tasks: []*tektonv1alpha1.Task{
    69  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
    70  					tb.TaskSpec(
    71  						tb.TaskInputs(
    72  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
    73  								tb.ResourceTargetPath("source"))),
    74  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
    75  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
    76  						tb.Step("some-image:0.0.1", tb.StepName("a-step-with-spaces-and-such"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
    77  					)),
    78  			},
    79  			structure: sh.PipelineStructure("somepipeline-1",
    80  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
    81  			),
    82  		},
    83  		{
    84  			name: "multiple_stages",
    85  			expected: sh.ParsedPipeline(
    86  				sh.PipelineAgent("some-image"),
    87  				sh.PipelineStage("A Working Stage",
    88  					sh.StageStep(
    89  						sh.StepCmd("echo"),
    90  						sh.StepArg("hello"), sh.StepArg("world")),
    91  				),
    92  				sh.PipelineStage("Another stage",
    93  					sh.StageStep(
    94  						sh.StepCmd("echo"),
    95  						sh.StepArg("again"))),
    96  			),
    97  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
    98  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
    99  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   100  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   101  				tb.PipelineTask("another-stage", "somepipeline-another-stage-1",
   102  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   103  						tb.From("a-working-stage")),
   104  					tb.RunAfter("a-working-stage")),
   105  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   106  			tasks: []*tektonv1alpha1.Task{
   107  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"), tb.TaskSpec(
   108  					tb.TaskInputs(
   109  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   110  							tb.ResourceTargetPath("source"))),
   111  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   112  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   113  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   114  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
   115  				)),
   116  				tb.Task("somepipeline-another-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Another stage"), tb.TaskSpec(
   117  					tb.TaskInputs(
   118  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   119  							tb.ResourceTargetPath("source"))),
   120  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   121  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo again"), tb.StepWorkingDir("/workspace/source")),
   122  				)),
   123  			},
   124  			structure: sh.PipelineStructure("somepipeline-1",
   125  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
   126  				sh.StructureStage("Another stage", sh.StructureStageTaskRef("somepipeline-another-stage-1"),
   127  					sh.StructureStagePrevious("A Working Stage")),
   128  			),
   129  		},
   130  		{
   131  			name: "nested_stages",
   132  			expected: sh.ParsedPipeline(
   133  				sh.PipelineAgent("some-image"),
   134  				sh.PipelineStage("Parent Stage",
   135  					sh.StageSequential("A Working Stage",
   136  						sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world"))),
   137  					sh.StageSequential("Another stage",
   138  						sh.StageStep(sh.StepCmd("echo"), sh.StepArg("again"))),
   139  				),
   140  			),
   141  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   142  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   143  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   144  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   145  				tb.PipelineTask("another-stage", "somepipeline-another-stage-1",
   146  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   147  						tb.From("a-working-stage")),
   148  					tb.RunAfter("a-working-stage")),
   149  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   150  			tasks: []*tektonv1alpha1.Task{
   151  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   152  					tb.TaskSpec(
   153  						tb.TaskInputs(
   154  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   155  						tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   156  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   157  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   158  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
   159  					)),
   160  				tb.Task("somepipeline-another-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Another stage"), tb.TaskSpec(
   161  					tb.TaskInputs(
   162  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   163  							tb.ResourceTargetPath("source"))),
   164  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   165  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo again"), tb.StepWorkingDir("/workspace/source")),
   166  				)),
   167  			},
   168  			structure: sh.PipelineStructure("somepipeline-1",
   169  				sh.StructureStage("Parent Stage",
   170  					sh.StructureStageStages("A Working Stage", "Another stage")),
   171  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1"),
   172  					sh.StructureStageDepth(1),
   173  					sh.StructureStageParent("Parent Stage")),
   174  				sh.StructureStage("Another stage", sh.StructureStageTaskRef("somepipeline-another-stage-1"),
   175  					sh.StructureStageDepth(1),
   176  					sh.StructureStageParent("Parent Stage"),
   177  					sh.StructureStagePrevious("A Working Stage")),
   178  			),
   179  		},
   180  		{
   181  			name: "parallel_stages",
   182  			expected: sh.ParsedPipeline(
   183  				sh.PipelineAgent("some-image"),
   184  				sh.PipelineStage("First Stage",
   185  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("first"))),
   186  				sh.PipelineStage("Parent Stage",
   187  					sh.StageParallel("A Working Stage",
   188  						sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world"))),
   189  					sh.StageParallel("Another stage",
   190  						sh.StageStep(sh.StepCmd("echo"), sh.StepArg("again"))),
   191  				),
   192  				sh.PipelineStage("Last Stage",
   193  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("last"))),
   194  			),
   195  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   196  				tb.PipelineTask("first-stage", "somepipeline-first-stage-1",
   197  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   198  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   199  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   200  					tb.PipelineTaskInputResource("workspace", "somepipeline", tb.From("first-stage")),
   201  					tb.RunAfter("first-stage")),
   202  				tb.PipelineTask("another-stage", "somepipeline-another-stage-1",
   203  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   204  						tb.From("first-stage")),
   205  					tb.RunAfter("first-stage")),
   206  				tb.PipelineTask("last-stage", "somepipeline-last-stage-1",
   207  					tb.PipelineTaskInputResource("workspace", "somepipeline", tb.From("first-stage")),
   208  					tb.RunAfter("a-working-stage", "another-stage")),
   209  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   210  			tasks: []*tektonv1alpha1.Task{
   211  				tb.Task("somepipeline-first-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("First Stage"), tb.TaskSpec(
   212  					tb.TaskInputs(
   213  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   214  							tb.ResourceTargetPath("source"))),
   215  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   216  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   217  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   218  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo first"), tb.StepWorkingDir("/workspace/source")),
   219  				)),
   220  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   221  					tb.TaskSpec(
   222  						tb.TaskInputs(
   223  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   224  								tb.ResourceTargetPath("source"))),
   225  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   226  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
   227  					)),
   228  				tb.Task("somepipeline-another-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Another stage"), tb.TaskSpec(
   229  					tb.TaskInputs(
   230  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   231  							tb.ResourceTargetPath("source"))),
   232  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   233  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo again"), tb.StepWorkingDir("/workspace/source")),
   234  				)),
   235  				tb.Task("somepipeline-last-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Last Stage"), tb.TaskSpec(
   236  					tb.TaskInputs(
   237  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   238  							tb.ResourceTargetPath("source"))),
   239  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   240  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo last"), tb.StepWorkingDir("/workspace/source")),
   241  				)),
   242  			},
   243  			structure: sh.PipelineStructure("somepipeline-1",
   244  				sh.StructureStage("First Stage", sh.StructureStageTaskRef("somepipeline-first-stage-1")),
   245  				sh.StructureStage("Parent Stage",
   246  					sh.StructureStageParallel("A Working Stage", "Another stage"),
   247  					sh.StructureStagePrevious("First Stage"),
   248  				),
   249  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1"),
   250  					sh.StructureStageDepth(1),
   251  					sh.StructureStageParent("Parent Stage"),
   252  				),
   253  				sh.StructureStage("Another stage", sh.StructureStageTaskRef("somepipeline-another-stage-1"),
   254  					sh.StructureStageDepth(1),
   255  					sh.StructureStageParent("Parent Stage"),
   256  				),
   257  				sh.StructureStage("Last Stage", sh.StructureStageTaskRef("somepipeline-last-stage-1"),
   258  					sh.StructureStagePrevious("Parent Stage")),
   259  			),
   260  		},
   261  		{
   262  			name: "parallel_and_nested_stages",
   263  			expected: sh.ParsedPipeline(
   264  				sh.PipelineAgent("some-image"),
   265  				sh.PipelineStage("First Stage",
   266  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("first"))),
   267  				sh.PipelineStage("Parent Stage",
   268  					sh.StageParallel("A Working Stage",
   269  						sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world"))),
   270  					sh.StageParallel("Nested In Parallel",
   271  						sh.StageSequential("Another stage",
   272  							sh.StageStep(sh.StepCmd("echo"), sh.StepArg("again"))),
   273  						sh.StageSequential("Some other stage",
   274  							sh.StageStep(sh.StepCmd("echo"), sh.StepArg("otherwise"))),
   275  					),
   276  				),
   277  				sh.PipelineStage("Last Stage",
   278  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("last"))),
   279  			),
   280  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   281  				tb.PipelineTask("first-stage", "somepipeline-first-stage-1",
   282  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   283  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   284  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   285  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   286  						tb.From("first-stage")),
   287  					tb.RunAfter("first-stage")),
   288  				tb.PipelineTask("another-stage", "somepipeline-another-stage-1",
   289  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   290  						tb.From("first-stage")),
   291  					tb.PipelineTaskOutputResource("workspace", "somepipeline"),
   292  					tb.RunAfter("first-stage")),
   293  				tb.PipelineTask("some-other-stage", "somepipeline-some-other-stage-1",
   294  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   295  						tb.From("another-stage")),
   296  					tb.RunAfter("another-stage")),
   297  				tb.PipelineTask("last-stage", "somepipeline-last-stage-1",
   298  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   299  						tb.From("first-stage")),
   300  					tb.RunAfter("a-working-stage", "some-other-stage")),
   301  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   302  			tasks: []*tektonv1alpha1.Task{
   303  				tb.Task("somepipeline-first-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("First Stage"), tb.TaskSpec(
   304  					tb.TaskInputs(
   305  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   306  							tb.ResourceTargetPath("source"))),
   307  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   308  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   309  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   310  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo first"), tb.StepWorkingDir("/workspace/source")),
   311  				)),
   312  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   313  					tb.TaskSpec(
   314  						tb.TaskInputs(
   315  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   316  								tb.ResourceTargetPath("source"))),
   317  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   318  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
   319  					)),
   320  				tb.Task("somepipeline-another-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Another stage"), tb.TaskSpec(
   321  					tb.TaskInputs(
   322  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   323  							tb.ResourceTargetPath("source"))),
   324  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   325  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   326  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo again"), tb.StepWorkingDir("/workspace/source")),
   327  				)),
   328  				tb.Task("somepipeline-some-other-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Some other stage"), tb.TaskSpec(
   329  					tb.TaskInputs(
   330  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   331  							tb.ResourceTargetPath("source"))),
   332  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   333  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo otherwise"), tb.StepWorkingDir("/workspace/source")),
   334  				)),
   335  				tb.Task("somepipeline-last-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Last Stage"), tb.TaskSpec(
   336  					tb.TaskInputs(
   337  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   338  							tb.ResourceTargetPath("source"))),
   339  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   340  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo last"), tb.StepWorkingDir("/workspace/source")),
   341  				)),
   342  			},
   343  			structure: sh.PipelineStructure("somepipeline-1",
   344  				sh.StructureStage("First Stage", sh.StructureStageTaskRef("somepipeline-first-stage-1")),
   345  				sh.StructureStage("Parent Stage",
   346  					sh.StructureStageParallel("A Working Stage", "Nested In Parallel"),
   347  					sh.StructureStagePrevious("First Stage"),
   348  				),
   349  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1"),
   350  					sh.StructureStageDepth(1),
   351  					sh.StructureStageParent("Parent Stage"),
   352  				),
   353  				sh.StructureStage("Nested In Parallel",
   354  					sh.StructureStageParent("Parent Stage"),
   355  					sh.StructureStageDepth(1),
   356  					sh.StructureStageStages("Another stage", "Some other stage"),
   357  				),
   358  				sh.StructureStage("Another stage", sh.StructureStageTaskRef("somepipeline-another-stage-1"),
   359  					sh.StructureStageDepth(2),
   360  					sh.StructureStageParent("Nested In Parallel"),
   361  				),
   362  				sh.StructureStage("Some other stage", sh.StructureStageTaskRef("somepipeline-some-other-stage-1"),
   363  					sh.StructureStageDepth(2),
   364  					sh.StructureStageParent("Nested In Parallel"),
   365  					sh.StructureStagePrevious("Another stage"),
   366  				),
   367  				sh.StructureStage("Last Stage", sh.StructureStageTaskRef("somepipeline-last-stage-1"),
   368  					sh.StructureStagePrevious("Parent Stage")),
   369  			),
   370  		},
   371  		{
   372  			name: "custom_workspaces",
   373  			expected: sh.ParsedPipeline(
   374  				sh.PipelineAgent("some-image"),
   375  				sh.PipelineStage("stage1",
   376  					sh.StageStep(sh.StepCmd("ls")),
   377  				),
   378  				sh.PipelineStage("stage2",
   379  					sh.StageOptions(
   380  						sh.StageOptionsWorkspace(customWorkspace),
   381  					),
   382  					sh.StageStep(sh.StepCmd("ls")),
   383  				),
   384  				sh.PipelineStage("stage3",
   385  					sh.StageOptions(
   386  						sh.StageOptionsWorkspace(defaultWorkspace),
   387  					),
   388  					sh.StageStep(sh.StepCmd("ls")),
   389  				),
   390  				sh.PipelineStage("stage4",
   391  					sh.StageOptions(
   392  						sh.StageOptionsWorkspace(customWorkspace),
   393  					),
   394  					sh.StageStep(sh.StepCmd("ls")),
   395  				),
   396  			),
   397  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   398  				tb.PipelineTask("stage1", "somepipeline-stage1-1",
   399  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   400  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   401  				tb.PipelineTask("stage2", "somepipeline-stage2-1",
   402  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   403  					tb.PipelineTaskOutputResource("workspace", "somepipeline"),
   404  					tb.RunAfter("stage1")),
   405  				tb.PipelineTask("stage3", "somepipeline-stage3-1",
   406  					tb.PipelineTaskInputResource("workspace", "somepipeline", tb.From("stage1")),
   407  					tb.PipelineTaskOutputResource("workspace", "somepipeline"),
   408  					tb.RunAfter("stage2")),
   409  				tb.PipelineTask("stage4", "somepipeline-stage4-1",
   410  					tb.PipelineTaskInputResource("workspace", "somepipeline", tb.From("stage2")),
   411  					tb.RunAfter("stage3")),
   412  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   413  			tasks: []*tektonv1alpha1.Task{
   414  				tb.Task("somepipeline-stage1-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage1"), tb.TaskSpec(
   415  					tb.TaskInputs(
   416  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   417  							tb.ResourceTargetPath("source"))),
   418  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   419  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   420  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   421  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   422  				)),
   423  				tb.Task("somepipeline-stage2-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage2"), tb.TaskSpec(
   424  					tb.TaskInputs(
   425  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   426  							tb.ResourceTargetPath("source"))),
   427  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   428  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   429  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   430  				)),
   431  				tb.Task("somepipeline-stage3-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage3"), tb.TaskSpec(
   432  					tb.TaskInputs(
   433  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   434  							tb.ResourceTargetPath("source"))),
   435  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   436  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   437  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   438  				)),
   439  				tb.Task("somepipeline-stage4-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage4"), tb.TaskSpec(
   440  					tb.TaskInputs(
   441  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   442  							tb.ResourceTargetPath("source"))),
   443  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   444  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   445  				)),
   446  			},
   447  			structure: sh.PipelineStructure("somepipeline-1",
   448  				sh.StructureStage("stage1", sh.StructureStageTaskRef("somepipeline-stage1-1")),
   449  				sh.StructureStage("stage2", sh.StructureStageTaskRef("somepipeline-stage2-1"), sh.StructureStagePrevious("stage1")),
   450  				sh.StructureStage("stage3", sh.StructureStageTaskRef("somepipeline-stage3-1"), sh.StructureStagePrevious("stage2")),
   451  				sh.StructureStage("stage4", sh.StructureStageTaskRef("somepipeline-stage4-1"), sh.StructureStagePrevious("stage3")),
   452  			),
   453  		},
   454  		{
   455  			name: "inherited_custom_workspaces",
   456  			expected: sh.ParsedPipeline(
   457  				sh.PipelineAgent("some-image"),
   458  				sh.PipelineStage("stage1",
   459  					sh.StageStep(sh.StepCmd("ls")),
   460  				),
   461  				sh.PipelineStage("stage2",
   462  					sh.StageOptions(
   463  						sh.StageOptionsWorkspace(customWorkspace),
   464  					),
   465  					sh.StageSequential("stage3",
   466  						sh.StageStep(sh.StepCmd("ls")),
   467  					),
   468  					sh.StageSequential("stage4",
   469  						sh.StageOptions(
   470  							sh.StageOptionsWorkspace(defaultWorkspace),
   471  						),
   472  						sh.StageStep(sh.StepCmd("ls")),
   473  					),
   474  					sh.StageSequential("stage5",
   475  						sh.StageStep(sh.StepCmd("ls")),
   476  					),
   477  				),
   478  			),
   479  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   480  				tb.PipelineTask("stage1", "somepipeline-stage1-1",
   481  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   482  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   483  				tb.PipelineTask("stage3", "somepipeline-stage3-1",
   484  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   485  					tb.PipelineTaskOutputResource("workspace", "somepipeline"),
   486  					tb.RunAfter("stage1")),
   487  				tb.PipelineTask("stage4", "somepipeline-stage4-1",
   488  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   489  						tb.From("stage1")),
   490  					tb.PipelineTaskOutputResource("workspace", "somepipeline"),
   491  					tb.RunAfter("stage3")),
   492  				tb.PipelineTask("stage5", "somepipeline-stage5-1",
   493  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   494  						tb.From("stage3")),
   495  					tb.RunAfter("stage4")),
   496  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   497  			tasks: []*tektonv1alpha1.Task{
   498  				tb.Task("somepipeline-stage1-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage1"), tb.TaskSpec(
   499  					tb.TaskInputs(
   500  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   501  							tb.ResourceTargetPath("source"))),
   502  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   503  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   504  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   505  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   506  				)),
   507  				tb.Task("somepipeline-stage3-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage3"), tb.TaskSpec(
   508  					tb.TaskInputs(
   509  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   510  							tb.ResourceTargetPath("source"))),
   511  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   512  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   513  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   514  				)),
   515  				tb.Task("somepipeline-stage4-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage4"), tb.TaskSpec(
   516  					tb.TaskInputs(
   517  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   518  							tb.ResourceTargetPath("source"))),
   519  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   520  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   521  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   522  				)),
   523  				tb.Task("somepipeline-stage5-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("stage5"), tb.TaskSpec(
   524  					tb.TaskInputs(
   525  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   526  							tb.ResourceTargetPath("source"))),
   527  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   528  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   529  				)),
   530  			},
   531  			structure: sh.PipelineStructure("somepipeline-1",
   532  				sh.StructureStage("stage1", sh.StructureStageTaskRef("somepipeline-stage1-1")),
   533  				sh.StructureStage("stage2",
   534  					sh.StructureStagePrevious("stage1"),
   535  					sh.StructureStageStages("stage3", "stage4", "stage5"),
   536  				),
   537  				sh.StructureStage("stage3", sh.StructureStageTaskRef("somepipeline-stage3-1"),
   538  					sh.StructureStageDepth(1),
   539  					sh.StructureStageParent("stage2")),
   540  				sh.StructureStage("stage4", sh.StructureStageTaskRef("somepipeline-stage4-1"),
   541  					sh.StructureStagePrevious("stage3"),
   542  					sh.StructureStageDepth(1),
   543  					sh.StructureStageParent("stage2")),
   544  				sh.StructureStage("stage5", sh.StructureStageTaskRef("somepipeline-stage5-1"),
   545  					sh.StructureStagePrevious("stage4"),
   546  					sh.StructureStageDepth(1),
   547  					sh.StructureStageParent("stage2")),
   548  			),
   549  		},
   550  		{
   551  			name: "environment_at_top_and_in_stage",
   552  			expected: sh.ParsedPipeline(
   553  				sh.PipelineAgent("some-image"),
   554  				sh.PipelineEnvVar("SOME_VAR", "A value for the env var"),
   555  				sh.PipelineStage("A stage with environment",
   556  					sh.StageEnvVar("SOME_OTHER_VAR", "A value for the other env var"),
   557  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("${SOME_OTHER_VAR}")),
   558  					sh.StageStep(
   559  						sh.StepCmd("echo"),
   560  						sh.StepArg("goodbye"), sh.StepArg("${SOME_VAR} and ${ANOTHER_VAR}"),
   561  						sh.StepEnvVar("SOME_VAR", "An overriding value"),
   562  						sh.StepEnvVar("ANOTHER_VAR", "Yet another variable"),
   563  					),
   564  				),
   565  			),
   566  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   567  				tb.PipelineTask("a-stage-with-environment", "somepipeline-a-stage-with-environment-1",
   568  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   569  				),
   570  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   571  			tasks: []*tektonv1alpha1.Task{
   572  				tb.Task("somepipeline-a-stage-with-environment-1", tb.TaskNamespace("jx"),
   573  					sh.TaskStageLabel("A stage with environment"),
   574  					tb.TaskSpec(
   575  						tb.TaskInputs(
   576  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   577  								tb.ResourceTargetPath("source"))),
   578  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
   579  							tb.StepEnvVar("SOME_OTHER_VAR", "A value for the other env var"), tb.StepEnvVar("SOME_VAR", "A value for the env var")),
   580  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
   581  							tb.StepEnvVar("SOME_OTHER_VAR", "A value for the other env var"), tb.StepEnvVar("SOME_VAR", "A value for the env var")),
   582  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello ${SOME_OTHER_VAR}"), tb.StepWorkingDir("/workspace/source"),
   583  							tb.StepEnvVar("SOME_OTHER_VAR", "A value for the other env var"), tb.StepEnvVar("SOME_VAR", "A value for the env var")),
   584  						tb.Step("some-image:0.0.1", tb.StepName("step3"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo goodbye ${SOME_VAR} and ${ANOTHER_VAR}"), tb.StepWorkingDir("/workspace/source"),
   585  							tb.StepEnvVar("ANOTHER_VAR", "Yet another variable"),
   586  							tb.StepEnvVar("SOME_OTHER_VAR", "A value for the other env var"),
   587  							tb.StepEnvVar("SOME_VAR", "An overriding value"),
   588  						),
   589  					)),
   590  			},
   591  			structure: sh.PipelineStructure("somepipeline-1",
   592  				sh.StructureStage("A stage with environment", sh.StructureStageTaskRef("somepipeline-a-stage-with-environment-1")),
   593  			),
   594  		},
   595  		{
   596  			name: "syntactic_sugar_step_and_a_command",
   597  			expected: sh.ParsedPipeline(
   598  				sh.PipelineAgent("some-image"),
   599  				sh.PipelineStage("A Working Stage",
   600  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world")),
   601  					sh.StageStep(sh.StepStep("some-step"),
   602  						sh.StepOptions(map[string]string{"firstParam": "some value", "secondParam": "some other value"})),
   603  				),
   604  			),
   605  			expectedErrorMsg: "syntactic sugar steps not yet supported",
   606  		},
   607  		{
   608  			name: "post",
   609  			expected: sh.ParsedPipeline(
   610  				sh.PipelineAgent("some-image"),
   611  				sh.PipelineStage("A Working Stage",
   612  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world")),
   613  					sh.StagePost(syntax.PostConditionSuccess,
   614  						sh.PostAction("mail", map[string]string{
   615  							"to":      "foo@bar.com",
   616  							"subject": "Yay, it passed",
   617  						})),
   618  					sh.StagePost(syntax.PostConditionFailure,
   619  						sh.PostAction("slack", map[string]string{
   620  							"whatever": "the",
   621  							"slack":    "config",
   622  							"actually": "is. =)",
   623  						})),
   624  					sh.StagePost(syntax.PostConditionAlways,
   625  						sh.PostAction("junit", map[string]string{
   626  							"pattern": "target/surefire-reports/**/*.xml",
   627  						}),
   628  					),
   629  				),
   630  			),
   631  			expectedErrorMsg: "post on stages not yet supported",
   632  		},
   633  		{
   634  			name: "top_level_and_stage_options",
   635  			expected: sh.ParsedPipeline(
   636  				sh.PipelineAgent("some-image"),
   637  				sh.PipelineOptions(
   638  					sh.PipelineOptionsTimeout(50, "minutes"),
   639  					sh.PipelineOptionsRetry(3),
   640  				),
   641  				sh.PipelineStage("A Working Stage",
   642  					sh.StageOptions(
   643  						sh.StageOptionsTimeout(5, "seconds"),
   644  						sh.StageOptionsRetry(4),
   645  						sh.StageOptionsStash("Some Files", "somedir/**/*"),
   646  						sh.StageOptionsUnstash("Earlier Files", "some/sub/dir"),
   647  					),
   648  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world")),
   649  				),
   650  			),
   651  			expectedErrorMsg: "Retry at top level not yet supported",
   652  		},
   653  		{
   654  			name: "stage_and_step_agent",
   655  			expected: sh.ParsedPipeline(
   656  				sh.PipelineStage("A Working Stage",
   657  					sh.StageAgent("some-image"),
   658  					sh.StageStep(
   659  						sh.StepCmd("echo"),
   660  						sh.StepArg("hello"), sh.StepArg("world"),
   661  						sh.StepAgent("some-other-image"),
   662  					),
   663  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("goodbye")),
   664  				),
   665  			),
   666  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   667  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   668  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   669  				),
   670  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   671  			tasks: []*tektonv1alpha1.Task{
   672  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   673  					tb.TaskSpec(
   674  						tb.TaskInputs(
   675  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   676  								tb.ResourceTargetPath("source"))),
   677  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   678  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   679  						tb.Step("some-other-image", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
   680  						tb.Step("some-image:0.0.1", tb.StepName("step3"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo goodbye"), tb.StepWorkingDir("/workspace/source")),
   681  					)),
   682  			},
   683  			structure: sh.PipelineStructure("somepipeline-1",
   684  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
   685  			),
   686  		},
   687  		{
   688  			name: "mangled_task_names",
   689  			expected: sh.ParsedPipeline(
   690  				sh.PipelineAgent("some-image"),
   691  				sh.PipelineStage(". -a- .",
   692  					sh.StageStep(sh.StepCmd("ls")),
   693  				),
   694  				sh.PipelineStage("Wööh!!!! - This is cool.",
   695  					sh.StageStep(sh.StepCmd("ls")),
   696  				),
   697  			),
   698  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   699  				tb.PipelineTask("a", "somepipeline-a-1",
   700  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   701  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   702  				tb.PipelineTask("wh-this-is-cool", "somepipeline-wh-this-is-cool-1",
   703  					tb.PipelineTaskInputResource("workspace", "somepipeline",
   704  						tb.From("a")),
   705  					tb.RunAfter("a")),
   706  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   707  			tasks: []*tektonv1alpha1.Task{
   708  				tb.Task("somepipeline-a-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("a"), tb.TaskSpec(
   709  					tb.TaskInputs(
   710  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   711  							tb.ResourceTargetPath("source"))),
   712  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   713  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   714  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   715  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   716  				)),
   717  				tb.Task("somepipeline-wh-this-is-cool-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("wh-this-is-cool"),
   718  					tb.TaskSpec(
   719  						tb.TaskInputs(
   720  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   721  								tb.ResourceTargetPath("source"))),
   722  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   723  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("ls"), tb.StepWorkingDir("/workspace/source")),
   724  					)),
   725  			},
   726  			structure: sh.PipelineStructure("somepipeline-1",
   727  				sh.StructureStage(". -a- .", sh.StructureStageTaskRef("somepipeline-a-1")),
   728  				sh.StructureStage("Wööh!!!! - This is cool.", sh.StructureStageTaskRef("somepipeline-wh-this-is-cool-1"), sh.StructureStagePrevious(". -a- .")),
   729  			),
   730  		},
   731  		{
   732  			name: "stage_timeout",
   733  			expected: sh.ParsedPipeline(
   734  				sh.PipelineAgent("some-image"),
   735  				sh.PipelineStage("A Working Stage",
   736  					sh.StageOptions(
   737  						sh.StageOptionsTimeout(50, "minutes"),
   738  					),
   739  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world")),
   740  				),
   741  			),
   742  			/* TODO: Stop erroring out once we figure out how to handle task timeouts again
   743  															pipeline: tb.Pipeline("somepipeline-1", tb.TaskNamespace("jx"), tb.PipelineSpec(
   744  																tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   745  																	tb.PipelineTaskInputResource("workspace", "somepipeline"),
   746  																	tb.PipelineTaskInputResource("temp-ordering-resource", "temp-ordering-resource"),
   747  																	tb.PipelineTaskOutputResource("workspace", "somepipeline")),
   748  									tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   749  			tasks: []*tektonv1alpha1.Task{
   750  																					tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), TaskStageLabel("A Working Stage"),
   751  																	tb.TaskSpec(
   752  												tb.TaskTimeout(50*time.Minute),
   753  																	tb.TaskInputs(
   754  																		tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   755  																			tb.ResourceTargetPath("source"))),
   756  						tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
   757  																	tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
   758  																)),
   759  															},*/
   760  			expectedErrorMsg: "Timeout on stage not yet supported",
   761  		},
   762  		{
   763  			name: "top_level_timeout",
   764  			expected: sh.ParsedPipeline(
   765  				sh.PipelineAgent("some-image"),
   766  				sh.PipelineOptions(
   767  					sh.PipelineOptionsTimeout(50, "minutes"),
   768  				),
   769  				sh.PipelineStage("A Working Stage",
   770  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world")),
   771  				),
   772  			),
   773  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   774  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   775  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   776  				),
   777  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   778  			tasks: []*tektonv1alpha1.Task{
   779  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   780  					tb.TaskSpec(
   781  						tb.TaskInputs(
   782  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   783  								tb.ResourceTargetPath("source"))),
   784  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
   785  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
   786  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
   787  					)),
   788  			},
   789  			structure: sh.PipelineStructure("somepipeline-1",
   790  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
   791  			),
   792  		},
   793  		{
   794  			name: "loop_step",
   795  			expected: sh.ParsedPipeline(
   796  				sh.PipelineEnvVar("LANGUAGE", "rust"),
   797  				sh.PipelineAgent("some-image"),
   798  				sh.PipelineStage("A Working Stage",
   799  					sh.StageEnvVar("DISTRO", "gentoo"),
   800  					sh.StageStep(
   801  						sh.StepLoop("LANGUAGE", []string{"maven", "gradle", "nodejs"},
   802  							sh.LoopStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("${LANGUAGE}")),
   803  							sh.LoopStep(sh.StepLoop("DISTRO", []string{"fedora", "ubuntu", "debian"},
   804  								sh.LoopStep(sh.StepCmd("echo"),
   805  									sh.StepArg("running"), sh.StepArg("${LANGUAGE}"),
   806  									sh.StepArg("on"), sh.StepArg("${DISTRO}")),
   807  							)),
   808  						),
   809  					),
   810  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("after")),
   811  				),
   812  			),
   813  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   814  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   815  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   816  				),
   817  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   818  			tasks: []*tektonv1alpha1.Task{
   819  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   820  					tb.TaskSpec(
   821  						tb.TaskInputs(
   822  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   823  								tb.ResourceTargetPath("source"))),
   824  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
   825  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "rust")),
   826  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
   827  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "rust")),
   828  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello ${LANGUAGE}"), tb.StepWorkingDir("/workspace/source"),
   829  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "maven")),
   830  						tb.Step("some-image:0.0.1", tb.StepName("step3"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   831  							tb.StepEnvVar("DISTRO", "fedora"), tb.StepEnvVar("LANGUAGE", "maven")),
   832  						tb.Step("some-image:0.0.1", tb.StepName("step4"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   833  							tb.StepEnvVar("DISTRO", "ubuntu"), tb.StepEnvVar("LANGUAGE", "maven")),
   834  						tb.Step("some-image:0.0.1", tb.StepName("step5"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   835  							tb.StepEnvVar("DISTRO", "debian"), tb.StepEnvVar("LANGUAGE", "maven")),
   836  						tb.Step("some-image:0.0.1", tb.StepName("step6"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello ${LANGUAGE}"), tb.StepWorkingDir("/workspace/source"),
   837  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "gradle")),
   838  						tb.Step("some-image:0.0.1", tb.StepName("step7"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   839  							tb.StepEnvVar("DISTRO", "fedora"), tb.StepEnvVar("LANGUAGE", "gradle")),
   840  						tb.Step("some-image:0.0.1", tb.StepName("step8"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   841  							tb.StepEnvVar("DISTRO", "ubuntu"), tb.StepEnvVar("LANGUAGE", "gradle")),
   842  						tb.Step("some-image:0.0.1", tb.StepName("step9"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   843  							tb.StepEnvVar("DISTRO", "debian"), tb.StepEnvVar("LANGUAGE", "gradle")),
   844  						tb.Step("some-image:0.0.1", tb.StepName("step10"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello ${LANGUAGE}"), tb.StepWorkingDir("/workspace/source"),
   845  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "nodejs")),
   846  						tb.Step("some-image:0.0.1", tb.StepName("step11"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   847  							tb.StepEnvVar("DISTRO", "fedora"), tb.StepEnvVar("LANGUAGE", "nodejs")),
   848  						tb.Step("some-image:0.0.1", tb.StepName("step12"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   849  							tb.StepEnvVar("DISTRO", "ubuntu"), tb.StepEnvVar("LANGUAGE", "nodejs")),
   850  						tb.Step("some-image:0.0.1", tb.StepName("step13"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo running ${LANGUAGE} on ${DISTRO}"), tb.StepWorkingDir("/workspace/source"),
   851  							tb.StepEnvVar("DISTRO", "debian"), tb.StepEnvVar("LANGUAGE", "nodejs")),
   852  						tb.Step("some-image:0.0.1", tb.StepName("step14"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello after"), tb.StepWorkingDir("/workspace/source"),
   853  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "rust")),
   854  					)),
   855  			},
   856  			structure: sh.PipelineStructure("somepipeline-1",
   857  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
   858  			),
   859  		},
   860  		{
   861  			name: "loop_step_with_name",
   862  			expected: sh.ParsedPipeline(
   863  				sh.PipelineEnvVar("LANGUAGE", "rust"),
   864  				sh.PipelineAgent("some-image"),
   865  				sh.PipelineStage("A Working Stage",
   866  					sh.StageEnvVar("DISTRO", "gentoo"),
   867  					sh.StageStep(
   868  						sh.StepLoop("LANGUAGE", []string{"maven", "gradle", "nodejs"},
   869  							sh.LoopStep(
   870  								sh.StepName("echo-step"),
   871  								sh.StepCmd("echo"),
   872  								sh.StepArg("hello"),
   873  								sh.StepArg("${LANGUAGE}")),
   874  						),
   875  					),
   876  				),
   877  			),
   878  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   879  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   880  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   881  				),
   882  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   883  			tasks: []*tektonv1alpha1.Task{
   884  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   885  					tb.TaskSpec(
   886  						tb.TaskInputs(
   887  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   888  								tb.ResourceTargetPath("source"))),
   889  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
   890  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "rust")),
   891  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
   892  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "rust")),
   893  						tb.Step("some-image:0.0.1", tb.StepName("echo-step1"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello ${LANGUAGE}"), tb.StepWorkingDir("/workspace/source"),
   894  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "maven")),
   895  						tb.Step("some-image:0.0.1", tb.StepName("echo-step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello ${LANGUAGE}"), tb.StepWorkingDir("/workspace/source"),
   896  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "gradle")),
   897  						tb.Step("some-image:0.0.1", tb.StepName("echo-step3"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello ${LANGUAGE}"), tb.StepWorkingDir("/workspace/source"),
   898  							tb.StepEnvVar("DISTRO", "gentoo"), tb.StepEnvVar("LANGUAGE", "nodejs")),
   899  					)),
   900  			},
   901  			structure: sh.PipelineStructure("somepipeline-1",
   902  				sh.StructureStage("A Working Stage",
   903  					sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
   904  			),
   905  		},
   906  		{
   907  			name: "loop_with_syntactic_sugar_step",
   908  			expected: sh.ParsedPipeline(
   909  				sh.PipelineAgent("some-image"),
   910  				sh.PipelineStage("A Working Stage",
   911  					sh.StageStep(
   912  						sh.StepLoop("LANGUAGE", []string{"maven", "gradle", "nodejs"},
   913  							sh.LoopStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("${LANGUAGE}")),
   914  							sh.LoopStep(sh.StepStep("some-step"),
   915  								sh.StepOptions(map[string]string{"firstParam": "some value", "secondParam": "some other value"})),
   916  						),
   917  					),
   918  				),
   919  			),
   920  			expectedErrorMsg: "syntactic sugar steps not yet supported",
   921  		},
   922  		{
   923  			name: "top_level_container_options",
   924  			expected: sh.ParsedPipeline(
   925  				sh.PipelineOptions(
   926  					sh.PipelineContainerOptions(
   927  						sh.ContainerResourceLimits("0.2", "128Mi"),
   928  						sh.ContainerResourceRequests("0.1", "64Mi"),
   929  					),
   930  				),
   931  				sh.PipelineAgent("some-image"),
   932  				sh.PipelineStage("A Working Stage",
   933  					sh.StageStep(
   934  						sh.StepCmd("echo"),
   935  						sh.StepArg("hello"), sh.StepArg("world"),
   936  					),
   937  				),
   938  			),
   939  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   940  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   941  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   942  				),
   943  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   944  			tasks: []*tektonv1alpha1.Task{
   945  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   946  					tb.TaskSpec(
   947  						tb.TaskInputs(
   948  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
   949  								tb.ResourceTargetPath("source"))),
   950  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
   951  							sh.StepResourceLimits("0.2", "128Mi"),
   952  							sh.StepResourceRequests("0.1", "64Mi"),
   953  						),
   954  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
   955  							sh.StepResourceLimits("0.2", "128Mi"),
   956  							sh.StepResourceRequests("0.1", "64Mi"),
   957  						),
   958  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source"),
   959  							sh.StepResourceLimits("0.2", "128Mi"),
   960  							sh.StepResourceRequests("0.1", "64Mi"),
   961  						),
   962  					)),
   963  			},
   964  			structure: sh.PipelineStructure("somepipeline-1",
   965  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
   966  			),
   967  		},
   968  		{
   969  			name: "stage_overrides_top_level_container_options",
   970  			expected: sh.ParsedPipeline(
   971  				sh.PipelineOptions(
   972  					sh.PipelineContainerOptions(
   973  						sh.ContainerResourceLimits("0.2", "128Mi"),
   974  						sh.ContainerResourceRequests("0.1", "64Mi"),
   975  					),
   976  				),
   977  				sh.PipelineAgent("some-image"),
   978  				sh.PipelineStage("A Working Stage",
   979  					sh.StageOptions(
   980  						sh.StageContainerOptions(
   981  							sh.ContainerResourceLimits("0.4", "256Mi"),
   982  							sh.ContainerResourceRequests("0.2", "128Mi"),
   983  						),
   984  					),
   985  					sh.StageStep(
   986  						sh.StepCmd("echo"),
   987  						sh.StepArg("hello"), sh.StepArg("world"),
   988  					),
   989  				),
   990  			),
   991  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
   992  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
   993  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
   994  				),
   995  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
   996  			tasks: []*tektonv1alpha1.Task{
   997  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
   998  					tb.TaskSpec(
   999  						tb.TaskInputs(
  1000  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1001  								tb.ResourceTargetPath("source"))),
  1002  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
  1003  							sh.StepResourceLimits("0.4", "256Mi"),
  1004  							sh.StepResourceRequests("0.2", "128Mi"),
  1005  						),
  1006  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
  1007  							sh.StepResourceLimits("0.4", "256Mi"),
  1008  							sh.StepResourceRequests("0.2", "128Mi"),
  1009  						),
  1010  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source"),
  1011  							sh.StepResourceLimits("0.4", "256Mi"),
  1012  							sh.StepResourceRequests("0.2", "128Mi"),
  1013  						),
  1014  					)),
  1015  			},
  1016  			structure: sh.PipelineStructure("somepipeline-1",
  1017  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1018  			),
  1019  		},
  1020  		{
  1021  			name: "merge_container_options",
  1022  			expected: sh.ParsedPipeline(
  1023  				sh.PipelineOptions(
  1024  					sh.PipelineContainerOptions(
  1025  						sh.ContainerResourceRequests("0.1", "64Mi"),
  1026  					),
  1027  				),
  1028  				sh.PipelineAgent("some-image"),
  1029  				sh.PipelineStage("A Working Stage",
  1030  					sh.StageOptions(
  1031  						sh.StageContainerOptions(
  1032  							sh.ContainerResourceLimits("0.4", "256Mi"),
  1033  						),
  1034  					),
  1035  					sh.StageStep(
  1036  						sh.StepCmd("echo"),
  1037  						sh.StepArg("hello"), sh.StepArg("world"),
  1038  					),
  1039  				),
  1040  			),
  1041  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1042  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1043  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1044  				),
  1045  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1046  			tasks: []*tektonv1alpha1.Task{
  1047  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
  1048  					tb.TaskSpec(
  1049  						tb.TaskInputs(
  1050  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1051  								tb.ResourceTargetPath("source"))),
  1052  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
  1053  							sh.StepResourceLimits("0.4", "256Mi"),
  1054  							sh.StepResourceRequests("0.1", "64Mi"),
  1055  						),
  1056  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
  1057  							sh.StepResourceLimits("0.4", "256Mi"),
  1058  							sh.StepResourceRequests("0.1", "64Mi"),
  1059  						),
  1060  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source"),
  1061  							sh.StepResourceLimits("0.4", "256Mi"),
  1062  							sh.StepResourceRequests("0.1", "64Mi"),
  1063  						),
  1064  					)),
  1065  			},
  1066  			structure: sh.PipelineStructure("somepipeline-1",
  1067  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1068  			),
  1069  		},
  1070  		{
  1071  			name: "stage_level_container_options",
  1072  			expected: sh.ParsedPipeline(
  1073  				sh.PipelineAgent("some-image"),
  1074  				sh.PipelineStage("A Working Stage",
  1075  					sh.StageOptions(
  1076  						sh.StageContainerOptions(
  1077  							sh.ContainerResourceLimits("0.2", "128Mi"),
  1078  							sh.ContainerResourceRequests("0.1", "64Mi"),
  1079  						),
  1080  					),
  1081  					sh.StageStep(
  1082  						sh.StepCmd("echo"),
  1083  						sh.StepArg("hello"), sh.StepArg("world"),
  1084  					),
  1085  				),
  1086  			),
  1087  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1088  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1089  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1090  				),
  1091  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1092  			tasks: []*tektonv1alpha1.Task{
  1093  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
  1094  					tb.TaskSpec(
  1095  						tb.TaskInputs(
  1096  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1097  								tb.ResourceTargetPath("source"))),
  1098  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
  1099  							sh.StepResourceLimits("0.2", "128Mi"),
  1100  							sh.StepResourceRequests("0.1", "64Mi"),
  1101  						),
  1102  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
  1103  							sh.StepResourceLimits("0.2", "128Mi"),
  1104  							sh.StepResourceRequests("0.1", "64Mi"),
  1105  						),
  1106  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source"),
  1107  							sh.StepResourceLimits("0.2", "128Mi"),
  1108  							sh.StepResourceRequests("0.1", "64Mi"),
  1109  						),
  1110  					)),
  1111  			},
  1112  			structure: sh.PipelineStructure("somepipeline-1",
  1113  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1114  			),
  1115  		},
  1116  		{
  1117  			name: "container_options_env_merge",
  1118  			expected: sh.ParsedPipeline(
  1119  				sh.PipelineAgent("some-image"),
  1120  				sh.PipelineOptions(
  1121  					sh.PipelineContainerOptions(
  1122  						tb.EnvVar("SOME_VAR", "A value for the env var"),
  1123  						tb.EnvVar("OVERRIDE_ENV", "Original value"),
  1124  						tb.EnvVar("OVERRIDE_STAGE_ENV", "Original value"),
  1125  					),
  1126  				),
  1127  				sh.PipelineEnvVar("SOME_OTHER_VAR", "A value for the other env var"),
  1128  				sh.PipelineEnvVar("OVERRIDE_ENV", "New value"),
  1129  				sh.PipelineStage("A Working Stage",
  1130  					sh.StageOptions(
  1131  						sh.StageContainerOptions(
  1132  							tb.EnvVar("ANOTHER_OVERRIDE_STAGE_ENV", "Original value"),
  1133  						),
  1134  					),
  1135  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world")),
  1136  					sh.StageEnvVar("OVERRIDE_STAGE_ENV", "New value"),
  1137  					sh.StageEnvVar("ANOTHER_OVERRIDE_STAGE_ENV", "New value"),
  1138  				),
  1139  			),
  1140  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1141  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1142  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1143  				),
  1144  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1145  			tasks: []*tektonv1alpha1.Task{
  1146  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"),
  1147  					sh.TaskStageLabel("A Working Stage"),
  1148  					tb.TaskSpec(
  1149  						tb.TaskInputs(
  1150  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1151  								tb.ResourceTargetPath("source"))),
  1152  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
  1153  							tb.StepEnvVar("ANOTHER_OVERRIDE_STAGE_ENV", "New value"),
  1154  							tb.StepEnvVar("SOME_VAR", "A value for the env var"),
  1155  							tb.StepEnvVar("OVERRIDE_ENV", "New value"),
  1156  							tb.StepEnvVar("OVERRIDE_STAGE_ENV", "New value"),
  1157  							tb.StepEnvVar("SOME_OTHER_VAR", "A value for the other env var"),
  1158  						),
  1159  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source"),
  1160  							tb.StepEnvVar("ANOTHER_OVERRIDE_STAGE_ENV", "New value"),
  1161  							tb.StepEnvVar("SOME_VAR", "A value for the env var"),
  1162  							tb.StepEnvVar("OVERRIDE_ENV", "New value"),
  1163  							tb.StepEnvVar("OVERRIDE_STAGE_ENV", "New value"),
  1164  							tb.StepEnvVar("SOME_OTHER_VAR", "A value for the other env var"),
  1165  						),
  1166  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source"),
  1167  							tb.StepEnvVar("ANOTHER_OVERRIDE_STAGE_ENV", "New value"),
  1168  							tb.StepEnvVar("OVERRIDE_ENV", "New value"),
  1169  							tb.StepEnvVar("OVERRIDE_STAGE_ENV", "New value"),
  1170  							tb.StepEnvVar("SOME_OTHER_VAR", "A value for the other env var"),
  1171  							tb.StepEnvVar("SOME_VAR", "A value for the env var"),
  1172  						),
  1173  					)),
  1174  			},
  1175  			structure: sh.PipelineStructure("somepipeline-1",
  1176  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1177  			),
  1178  		},
  1179  		{
  1180  			name: "dir_on_pipeline_and_stage",
  1181  			expected: sh.ParsedPipeline(
  1182  				sh.PipelineAgent("some-image"),
  1183  				sh.PipelineDir("a-relative-dir"),
  1184  				sh.PipelineStage("A Working Stage",
  1185  					sh.StageStep(
  1186  						sh.StepCmd("echo"),
  1187  						sh.StepArg("hello"), sh.StepArg("world")),
  1188  				),
  1189  				sh.PipelineStage("Another stage",
  1190  					sh.StageDir("/an/absolute/dir"),
  1191  					sh.StageStep(
  1192  						sh.StepCmd("echo"),
  1193  						sh.StepArg("again")),
  1194  					sh.StageStep(
  1195  						sh.StepCmd("echo"),
  1196  						sh.StepArg("in another dir"),
  1197  						sh.StepDir("another-relative-dir/with/a/subdir"))),
  1198  			),
  1199  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1200  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1201  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1202  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
  1203  				tb.PipelineTask("another-stage", "somepipeline-another-stage-1",
  1204  					tb.PipelineTaskInputResource("workspace", "somepipeline",
  1205  						tb.From("a-working-stage")),
  1206  					tb.RunAfter("a-working-stage")),
  1207  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1208  			tasks: []*tektonv1alpha1.Task{
  1209  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"), tb.TaskSpec(
  1210  					tb.TaskInputs(
  1211  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1212  							tb.ResourceTargetPath("source"))),
  1213  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
  1214  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
  1215  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
  1216  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"),
  1217  						tb.StepArgs("cd /workspace/source/a-relative-dir && echo hello world"), tb.StepWorkingDir("/workspace/source")),
  1218  				)),
  1219  				tb.Task("somepipeline-another-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Another stage"), tb.TaskSpec(
  1220  					tb.TaskInputs(
  1221  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1222  							tb.ResourceTargetPath("source"))),
  1223  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
  1224  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"),
  1225  						tb.StepArgs("cd /an/absolute/dir && echo again"),
  1226  						tb.StepWorkingDir("/workspace/source")),
  1227  					tb.Step("some-image:0.0.1", tb.StepName("step3"), tb.StepCommand("/bin/sh", "-c"),
  1228  						tb.StepArgs("cd /workspace/source/another-relative-dir/with/a/subdir && echo in another dir"),
  1229  						tb.StepWorkingDir("/workspace/source")),
  1230  				)),
  1231  			},
  1232  			structure: sh.PipelineStructure("somepipeline-1",
  1233  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1234  				sh.StructureStage("Another stage", sh.StructureStageTaskRef("somepipeline-another-stage-1"),
  1235  					sh.StructureStagePrevious("A Working Stage")),
  1236  			),
  1237  		},
  1238  		{
  1239  			name: "sidecars",
  1240  			expected: sh.ParsedPipeline(
  1241  				sh.PipelineOptions(
  1242  					sh.PipelineSidecar(&corev1.Container{
  1243  						Name:  "top-level-sidecar",
  1244  						Image: "top-level/sidecar:tag",
  1245  					}),
  1246  				),
  1247  				sh.PipelineAgent("some-image"),
  1248  				sh.PipelineStage("A Working Stage",
  1249  					sh.StageOptions(
  1250  						sh.StageSidecar(&corev1.Container{
  1251  							Name:  "stage-level-sidecar",
  1252  							Image: "stage-level/sidecar:latest",
  1253  							VolumeMounts: []corev1.VolumeMount{
  1254  								{
  1255  									Name:      "shared-volume",
  1256  									MountPath: "/shared",
  1257  								},
  1258  							},
  1259  						}),
  1260  						sh.StageVolume(&corev1.Volume{
  1261  							Name: "shared-volume",
  1262  							VolumeSource: corev1.VolumeSource{
  1263  								EmptyDir: &corev1.EmptyDirVolumeSource{},
  1264  							},
  1265  						}),
  1266  						sh.StageContainerOptions(
  1267  							sh.ContainerVolumeMount("shared-volume", "/mnt/shared"),
  1268  						),
  1269  					),
  1270  					sh.StageStep(
  1271  						sh.StepCmd("echo"),
  1272  						sh.StepArg("hello"), sh.StepArg("world"),
  1273  						sh.StepName("A Step With Spaces And Such"),
  1274  					),
  1275  				),
  1276  			),
  1277  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1278  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1279  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1280  				),
  1281  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1282  			tasks: []*tektonv1alpha1.Task{
  1283  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
  1284  					tb.TaskSpec(
  1285  						tb.TaskInputs(
  1286  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1287  								tb.ResourceTargetPath("source"))),
  1288  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
  1289  							sh.StepVolumeMount("shared-volume", "/mnt/shared"),
  1290  						),
  1291  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"),
  1292  							tb.StepCommand("jx"),
  1293  							tb.StepArgs("step", "git", "merge", "--verbose"),
  1294  							tb.StepWorkingDir("/workspace/source"),
  1295  							sh.StepVolumeMount("shared-volume", "/mnt/shared"),
  1296  						),
  1297  						tb.Step("some-image:0.0.1", tb.StepName("a-step-with-spaces-and-such"),
  1298  							tb.StepCommand("/bin/sh", "-c"),
  1299  							tb.StepArgs("echo hello world"),
  1300  							tb.StepWorkingDir("/workspace/source"),
  1301  							sh.StepVolumeMount("shared-volume", "/mnt/shared"),
  1302  						),
  1303  						tb.Sidecar("stage-level-sidecar", "stage-level/sidecar:latest",
  1304  							tb.VolumeMount("shared-volume", "/shared"),
  1305  						),
  1306  						tb.Sidecar("top-level-sidecar", "top-level/sidecar:tag"),
  1307  						tb.TaskVolume("shared-volume", tb.VolumeSource(corev1.VolumeSource{
  1308  							EmptyDir: &corev1.EmptyDirVolumeSource{},
  1309  						})),
  1310  					)),
  1311  			},
  1312  			structure: sh.PipelineStructure("somepipeline-1",
  1313  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1314  			),
  1315  		},
  1316  		{
  1317  			name: "volumes",
  1318  			expected: sh.ParsedPipeline(
  1319  				sh.PipelineOptions(
  1320  					sh.PipelineVolume(&corev1.Volume{
  1321  						Name: "top-level-volume",
  1322  						VolumeSource: corev1.VolumeSource{
  1323  							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
  1324  								ClaimName: "top-level-volume",
  1325  								ReadOnly:  true,
  1326  							},
  1327  						},
  1328  					}),
  1329  				),
  1330  				sh.PipelineAgent("some-image"),
  1331  				sh.PipelineStage("A Working Stage",
  1332  					sh.StageOptions(
  1333  						sh.StageVolume(&corev1.Volume{
  1334  							Name: "stage-level-volume",
  1335  							VolumeSource: corev1.VolumeSource{
  1336  								GCEPersistentDisk: &corev1.GCEPersistentDiskVolumeSource{
  1337  									PDName: "stage-level-volume",
  1338  								},
  1339  							},
  1340  						}),
  1341  						sh.StageContainerOptions(
  1342  							sh.ContainerVolumeMount("top-level-volume", "/mnt/top-level-volume"),
  1343  							sh.ContainerVolumeMount("stage-level-volume", "/mnt/stage-level-volume"),
  1344  						),
  1345  					),
  1346  					sh.StageStep(
  1347  						sh.StepCmd("echo"),
  1348  						sh.StepArg("hello"), sh.StepArg("world"),
  1349  						sh.StepName("A Step With Spaces And Such"),
  1350  					),
  1351  				),
  1352  			),
  1353  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1354  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1355  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1356  				),
  1357  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1358  			tasks: []*tektonv1alpha1.Task{
  1359  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
  1360  					tb.TaskSpec(
  1361  						tb.TaskInputs(
  1362  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1363  								tb.ResourceTargetPath("source"))),
  1364  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source"),
  1365  							sh.StepVolumeMount("top-level-volume", "/mnt/top-level-volume"),
  1366  							sh.StepVolumeMount("stage-level-volume", "/mnt/stage-level-volume"),
  1367  						),
  1368  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"),
  1369  							tb.StepCommand("jx"),
  1370  							tb.StepArgs("step", "git", "merge", "--verbose"),
  1371  							tb.StepWorkingDir("/workspace/source"),
  1372  							sh.StepVolumeMount("top-level-volume", "/mnt/top-level-volume"),
  1373  							sh.StepVolumeMount("stage-level-volume", "/mnt/stage-level-volume"),
  1374  						),
  1375  						tb.Step("some-image:0.0.1", tb.StepName("a-step-with-spaces-and-such"),
  1376  							tb.StepCommand("/bin/sh", "-c"),
  1377  							tb.StepArgs("echo hello world"),
  1378  							tb.StepWorkingDir("/workspace/source"),
  1379  							sh.StepVolumeMount("top-level-volume", "/mnt/top-level-volume"),
  1380  							sh.StepVolumeMount("stage-level-volume", "/mnt/stage-level-volume"),
  1381  						),
  1382  						tb.TaskVolume("stage-level-volume", tb.VolumeSource(corev1.VolumeSource{
  1383  							GCEPersistentDisk: &corev1.GCEPersistentDiskVolumeSource{
  1384  								PDName: "stage-level-volume",
  1385  							},
  1386  						})),
  1387  						tb.TaskVolume("top-level-volume", tb.VolumeSource(corev1.VolumeSource{
  1388  							PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
  1389  								ClaimName: "top-level-volume",
  1390  								ReadOnly:  true,
  1391  							},
  1392  						})),
  1393  					)),
  1394  			},
  1395  			structure: sh.PipelineStructure("somepipeline-1",
  1396  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1397  			),
  1398  		},
  1399  		{
  1400  			name: "node_distributed_parallel_stages",
  1401  			expected: sh.ParsedPipeline(
  1402  				sh.PipelineOptions(sh.PipelineOptionsDistributeParallelAcrossNodes(true)),
  1403  				sh.PipelineAgent("some-image"),
  1404  				sh.PipelineStage("First Stage",
  1405  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("first"))),
  1406  				sh.PipelineStage("Parent Stage",
  1407  					sh.StageParallel("A Working Stage",
  1408  						sh.StageStep(sh.StepCmd("echo"), sh.StepArg("hello"), sh.StepArg("world"))),
  1409  					sh.StageParallel("Another stage",
  1410  						sh.StageStep(sh.StepCmd("echo"), sh.StepArg("again"))),
  1411  				),
  1412  				sh.PipelineStage("Last Stage",
  1413  					sh.StageStep(sh.StepCmd("echo"), sh.StepArg("last"))),
  1414  			),
  1415  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1416  				tb.PipelineTask("first-stage", "somepipeline-first-stage-1",
  1417  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1418  					tb.PipelineTaskOutputResource("workspace", "somepipeline")),
  1419  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1420  					tb.PipelineTaskInputResource("workspace", "somepipeline", tb.From("first-stage")),
  1421  					tb.RunAfter("first-stage")),
  1422  				tb.PipelineTask("another-stage", "somepipeline-another-stage-1",
  1423  					tb.PipelineTaskInputResource("workspace", "somepipeline",
  1424  						tb.From("first-stage")),
  1425  					tb.RunAfter("first-stage")),
  1426  				tb.PipelineTask("last-stage", "somepipeline-last-stage-1",
  1427  					tb.PipelineTaskInputResource("workspace", "somepipeline", tb.From("first-stage")),
  1428  					tb.RunAfter("a-working-stage", "another-stage")),
  1429  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1430  			tasks: []*tektonv1alpha1.Task{
  1431  				tb.Task("somepipeline-first-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("First Stage"), tb.TaskSpec(
  1432  					tb.TaskInputs(
  1433  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1434  							tb.ResourceTargetPath("source"))),
  1435  					tb.TaskOutputs(sh.OutputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit, tb.ResourceTargetPath("source"))),
  1436  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
  1437  					tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
  1438  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo first"), tb.StepWorkingDir("/workspace/source")),
  1439  				)),
  1440  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
  1441  					tb.TaskSpec(
  1442  						tb.TaskInputs(
  1443  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1444  								tb.ResourceTargetPath("source"))),
  1445  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
  1446  						tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
  1447  					)),
  1448  				tb.Task("somepipeline-another-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Another stage"), tb.TaskSpec(
  1449  					tb.TaskInputs(
  1450  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1451  							tb.ResourceTargetPath("source"))),
  1452  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
  1453  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo again"), tb.StepWorkingDir("/workspace/source")),
  1454  				)),
  1455  				tb.Task("somepipeline-last-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("Last Stage"), tb.TaskSpec(
  1456  					tb.TaskInputs(
  1457  						tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1458  							tb.ResourceTargetPath("source"))),
  1459  					tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
  1460  					tb.Step("some-image:0.0.1", tb.StepName("step2"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo last"), tb.StepWorkingDir("/workspace/source")),
  1461  				)),
  1462  			},
  1463  			structure: sh.PipelineStructure("somepipeline-1",
  1464  				sh.StructureStage("First Stage", sh.StructureStageTaskRef("somepipeline-first-stage-1")),
  1465  				sh.StructureStage("Parent Stage",
  1466  					sh.StructureStageParallel("A Working Stage", "Another stage"),
  1467  					sh.StructureStagePrevious("First Stage"),
  1468  				),
  1469  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1"),
  1470  					sh.StructureStageDepth(1),
  1471  					sh.StructureStageParent("Parent Stage"),
  1472  				),
  1473  				sh.StructureStage("Another stage", sh.StructureStageTaskRef("somepipeline-another-stage-1"),
  1474  					sh.StructureStageDepth(1),
  1475  					sh.StructureStageParent("Parent Stage"),
  1476  				),
  1477  				sh.StructureStage("Last Stage", sh.StructureStageTaskRef("somepipeline-last-stage-1"),
  1478  					sh.StructureStagePrevious("Parent Stage")),
  1479  			),
  1480  		},
  1481  		{
  1482  			name: "tolerations",
  1483  			expected: sh.ParsedPipeline(
  1484  				sh.PipelineAgent("some-image"),
  1485  				sh.PipelineOptions(
  1486  					sh.PipelineTolerations([]corev1.Toleration{{
  1487  						Key:      "some-key",
  1488  						Operator: "Exists",
  1489  						Effect:   "NoSchedule",
  1490  					}}),
  1491  					sh.PipelinePodLabels(map[string]string{
  1492  						"foo":   "bar",
  1493  						"fruit": "apple",
  1494  					}),
  1495  				),
  1496  				sh.PipelineStage("A Working Stage",
  1497  					sh.StageStep(
  1498  						sh.StepCmd("echo"),
  1499  						sh.StepArg("hello"), sh.StepArg("world"),
  1500  						sh.StepName("A Step With Spaces And Such"),
  1501  					),
  1502  				),
  1503  			),
  1504  			pipeline: tb.Pipeline("somepipeline-1", tb.PipelineNamespace("jx"), tb.PipelineSpec(
  1505  				tb.PipelineTask("a-working-stage", "somepipeline-a-working-stage-1",
  1506  					tb.PipelineTaskInputResource("workspace", "somepipeline"),
  1507  				),
  1508  				tb.PipelineDeclaredResource("somepipeline", tektonv1alpha1.PipelineResourceTypeGit))),
  1509  			tasks: []*tektonv1alpha1.Task{
  1510  				tb.Task("somepipeline-a-working-stage-1", tb.TaskNamespace("jx"), sh.TaskStageLabel("A Working Stage"),
  1511  					tb.TaskSpec(
  1512  						tb.TaskInputs(
  1513  							tb.InputsResource("workspace", tektonv1alpha1.PipelineResourceTypeGit,
  1514  								tb.ResourceTargetPath("source"))),
  1515  						tb.Step(resolvedGitMergeImage, tb.StepName("setup-builder-home"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("[ -d /builder/home ] || mkdir -p /builder && ln -s /tekton/home /builder/home"), tb.StepWorkingDir("/workspace/source")),
  1516  						tb.Step(resolvedGitMergeImage, tb.StepName("git-merge"), tb.StepCommand("jx"), tb.StepArgs("step", "git", "merge", "--verbose"), tb.StepWorkingDir("/workspace/source")),
  1517  						tb.Step("some-image:0.0.1", tb.StepName("a-step-with-spaces-and-such"), tb.StepCommand("/bin/sh", "-c"), tb.StepArgs("echo hello world"), tb.StepWorkingDir("/workspace/source")),
  1518  					)),
  1519  			},
  1520  			structure: sh.PipelineStructure("somepipeline-1",
  1521  				sh.StructureStage("A Working Stage", sh.StructureStageTaskRef("somepipeline-a-working-stage-1")),
  1522  			),
  1523  		},
  1524  	}
  1525  
  1526  	for _, tt := range tests {
  1527  		t.Run(tt.name, func(t *testing.T) {
  1528  			testVersionsDir := filepath.Join("test_data", "stable_versions")
  1529  			projectConfig, fn, err := config.LoadProjectConfig(filepath.Join("test_data", tt.name))
  1530  			if err != nil {
  1531  				t.Fatalf("Failed to parse YAML for %s: %q", tt.name, err)
  1532  			}
  1533  
  1534  			if projectConfig.PipelineConfig == nil {
  1535  				t.Fatalf("PipelineConfig at %s is nil: %+v", fn, projectConfig)
  1536  			}
  1537  			if &projectConfig.PipelineConfig.Pipelines == nil {
  1538  				t.Fatalf("Pipelines at %s is nil: %+v", fn, projectConfig.PipelineConfig)
  1539  			}
  1540  			if projectConfig.PipelineConfig.Pipelines.Release == nil {
  1541  				t.Fatalf("Release at %s is nil: %+v", fn, projectConfig.PipelineConfig.Pipelines)
  1542  			}
  1543  			if projectConfig.PipelineConfig.Pipelines.Release.Pipeline == nil {
  1544  				t.Fatalf("Pipeline at %s is nil: %+v", fn, projectConfig.PipelineConfig.Pipelines.Release)
  1545  			}
  1546  			parsed := projectConfig.PipelineConfig.Pipelines.Release.Pipeline
  1547  
  1548  			if d, _ := kmp.SafeDiff(tt.expected, parsed); d != "" && tt.expected != nil {
  1549  				t.Errorf("Parsed ParsedPipeline did not match expected: %s", d)
  1550  			}
  1551  
  1552  			validateErr := parsed.Validate(ctx)
  1553  			if validateErr != nil && tt.validationErrorMsg == "" {
  1554  				t.Errorf("Validation failed: %s", validateErr)
  1555  			}
  1556  
  1557  			if validateErr != nil && tt.validationErrorMsg != "" {
  1558  				if tt.validationErrorMsg != validateErr.Details {
  1559  					t.Errorf("Validation Error failed: '%s', '%s'", validateErr.Details, tt.validationErrorMsg)
  1560  				}
  1561  			}
  1562  
  1563  			crdParams := syntax.CRDsFromPipelineParams{
  1564  				PipelineIdentifier: "somepipeline",
  1565  				BuildIdentifier:    "1",
  1566  				Namespace:          "jx",
  1567  				VersionsDir:        testVersionsDir,
  1568  				SourceDir:          "source",
  1569  				DefaultImage:       "",
  1570  				InterpretMode:      false,
  1571  			}
  1572  			pipeline, tasks, structure, err := parsed.GenerateCRDs(crdParams)
  1573  
  1574  			if err != nil {
  1575  				if tt.expectedErrorMsg != "" {
  1576  					if d := cmp.Diff(tt.expectedErrorMsg, err.Error()); d != "" {
  1577  						t.Fatalf("CRD generation error did not meet expectation: %s", d)
  1578  					}
  1579  				} else {
  1580  					t.Fatalf("Error generating CRDs: %s", err)
  1581  				}
  1582  			}
  1583  
  1584  			if tt.expectedErrorMsg == "" && tt.pipeline != nil {
  1585  				pipeline.TypeMeta = metav1.TypeMeta{}
  1586  				if d := cmp.Diff(tt.pipeline, pipeline); d != "" {
  1587  					t.Errorf("Generated Pipeline did not match expected: %s", d)
  1588  				}
  1589  
  1590  				if err := pipeline.Spec.Validate(ctx); err != nil {
  1591  					t.Errorf("PipelineSpec.Validate(ctx) = %v", err)
  1592  				}
  1593  
  1594  				for _, task := range tasks {
  1595  					task.TypeMeta = metav1.TypeMeta{}
  1596  				}
  1597  				if d, _ := kmp.SafeDiff(tt.tasks, tasks); d != "" {
  1598  					t.Errorf("Generated Tasks did not match expected: %s", d)
  1599  				}
  1600  
  1601  				for _, task := range tasks {
  1602  					if err := task.Spec.Validate(ctx); err != nil {
  1603  						t.Errorf("TaskSpec.Validate(ctx) = %v", err)
  1604  					}
  1605  				}
  1606  
  1607  				if tt.structure != nil {
  1608  					if d := cmp.Diff(tt.structure, structure); d != "" {
  1609  						t.Errorf("Generated PipelineStructure did not match expected: %s", d)
  1610  					}
  1611  				}
  1612  			}
  1613  		})
  1614  	}
  1615  }
  1616  
  1617  func TestFailedValidation(t *testing.T) {
  1618  	ctx := context.Background()
  1619  	tests := []struct {
  1620  		name          string
  1621  		expectedError error
  1622  	}{
  1623  		/* TODO: Once we figure out how to differentiate between an empty agent and no agent specified...
  1624  		{
  1625  			name: "empty_agent",
  1626  			expectedError: &apis.FieldError{
  1627  				Message: "Invalid apiVersion format: must be 'v(digits).(digits)",
  1628  				Paths:   []string{"apiVersion"},
  1629  			},
  1630  		},
  1631  		*/
  1632  		{
  1633  			name: "agent_with_both_image_and_label",
  1634  			expectedError: apis.ErrMultipleOneOf("label", "image").
  1635  				ViaField("agent"),
  1636  		},
  1637  		{
  1638  			name:          "no_stages",
  1639  			expectedError: apis.ErrMissingField("stages"),
  1640  		},
  1641  		{
  1642  			name:          "no_steps_stages_or_parallel",
  1643  			expectedError: apis.ErrMissingOneOf("steps", "stages", "parallel").ViaFieldIndex("stages", 0),
  1644  		},
  1645  		{
  1646  			name:          "steps_and_stages",
  1647  			expectedError: apis.ErrMultipleOneOf("steps", "stages", "parallel").ViaFieldIndex("stages", 0),
  1648  		},
  1649  		{
  1650  			name:          "steps_and_parallel",
  1651  			expectedError: apis.ErrMultipleOneOf("steps", "stages", "parallel").ViaFieldIndex("stages", 0),
  1652  		},
  1653  		{
  1654  			name:          "stages_and_parallel",
  1655  			expectedError: apis.ErrMultipleOneOf("steps", "stages", "parallel").ViaFieldIndex("stages", 0),
  1656  		},
  1657  		{
  1658  			name:          "step_without_command_step_or_loop",
  1659  			expectedError: apis.ErrMissingOneOf("command", "step", "loop").ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1660  		},
  1661  		{
  1662  			name:          "step_with_both_command_and_step",
  1663  			expectedError: apis.ErrMultipleOneOf("command", "step", "loop").ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1664  		},
  1665  		{
  1666  			name:          "step_with_both_command_and_loop",
  1667  			expectedError: apis.ErrMultipleOneOf("command", "step", "loop").ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1668  		},
  1669  		{
  1670  			name: "step_with_command_and_options",
  1671  			expectedError: (&apis.FieldError{
  1672  				Message: "Cannot set options for a command or a loop",
  1673  				Paths:   []string{"options"},
  1674  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1675  		},
  1676  		{
  1677  			name: "step_with_step_and_arguments",
  1678  			expectedError: (&apis.FieldError{
  1679  				Message: "Cannot set command-line arguments for a step or a loop",
  1680  				Paths:   []string{"args"},
  1681  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1682  		},
  1683  		{
  1684  			name: "step_with_loop_and_options",
  1685  			expectedError: (&apis.FieldError{
  1686  				Message: "Cannot set options for a command or a loop",
  1687  				Paths:   []string{"options"},
  1688  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1689  		},
  1690  		{
  1691  			name: "step_with_loop_and_arguments",
  1692  			expectedError: (&apis.FieldError{
  1693  				Message: "Cannot set command-line arguments for a step or a loop",
  1694  				Paths:   []string{"args"},
  1695  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1696  		},
  1697  		{
  1698  			name: "no_parent_or_stage_agent",
  1699  			expectedError: (&apis.FieldError{
  1700  				Message: "No agent specified for stage or for its parent(s)",
  1701  				Paths:   []string{"agent"},
  1702  			}).ViaFieldIndex("stages", 0),
  1703  		},
  1704  		{
  1705  			name: "top_level_timeout_without_time",
  1706  			expectedError: (&apis.FieldError{
  1707  				Message: "Timeout must be greater than zero",
  1708  				Paths:   []string{"time"},
  1709  			}).ViaField("timeout").ViaField("options"),
  1710  		},
  1711  		{
  1712  			name: "stage_timeout_without_time",
  1713  			expectedError: (&apis.FieldError{
  1714  				Message: "Timeout must be greater than zero",
  1715  				Paths:   []string{"time"},
  1716  			}).ViaField("timeout").ViaField("options").ViaFieldIndex("stages", 0),
  1717  		},
  1718  		{
  1719  			name: "top_level_timeout_with_invalid_unit",
  1720  			expectedError: (&apis.FieldError{
  1721  				Message: "years is not a valid time unit. Valid time units are seconds, minutes, hours, days",
  1722  				Paths:   []string{"unit"},
  1723  			}).ViaField("timeout").ViaField("options"),
  1724  		},
  1725  		{
  1726  			name: "stage_timeout_with_invalid_unit",
  1727  			expectedError: (&apis.FieldError{
  1728  				Message: "years is not a valid time unit. Valid time units are seconds, minutes, hours, days",
  1729  				Paths:   []string{"unit"},
  1730  			}).ViaField("timeout").ViaField("options").ViaFieldIndex("stages", 0),
  1731  		},
  1732  		{
  1733  			name: "top_level_timeout_with_invalid_time",
  1734  			expectedError: (&apis.FieldError{
  1735  				Message: "Timeout must be greater than zero",
  1736  				Paths:   []string{"time"},
  1737  			}).ViaField("timeout").ViaField("options"),
  1738  		},
  1739  		{
  1740  			name: "stage_timeout_with_invalid_time",
  1741  			expectedError: (&apis.FieldError{
  1742  				Message: "Timeout must be greater than zero",
  1743  				Paths:   []string{"time"},
  1744  			}).ViaField("timeout").ViaField("options").ViaFieldIndex("stages", 0),
  1745  		},
  1746  		{
  1747  			name: "top_level_retry_with_invalid_count",
  1748  			expectedError: (&apis.FieldError{
  1749  				Message: "Retry count cannot be negative",
  1750  				Paths:   []string{"retry"},
  1751  			}).ViaField("options"),
  1752  		},
  1753  		{
  1754  			name: "stage_retry_with_invalid_count",
  1755  			expectedError: (&apis.FieldError{
  1756  				Message: "Retry count cannot be negative",
  1757  				Paths:   []string{"retry"},
  1758  			}).ViaField("options").ViaFieldIndex("stages", 0),
  1759  		},
  1760  		{
  1761  			name: "stash_without_name",
  1762  			expectedError: (&apis.FieldError{
  1763  				Message: "The stash name must be provided",
  1764  				Paths:   []string{"name"},
  1765  			}).ViaField("stash").ViaField("options").ViaFieldIndex("stages", 0),
  1766  		},
  1767  		{
  1768  			name: "stash_without_files",
  1769  			expectedError: (&apis.FieldError{
  1770  				Message: "files to stash must be provided",
  1771  				Paths:   []string{"files"},
  1772  			}).ViaField("stash").ViaField("options").ViaFieldIndex("stages", 0),
  1773  		},
  1774  		{
  1775  			name: "unstash_without_name",
  1776  			expectedError: (&apis.FieldError{
  1777  				Message: "The unstash name must be provided",
  1778  				Paths:   []string{"name"},
  1779  			}).ViaField("unstash").ViaField("options").ViaFieldIndex("stages", 0),
  1780  		},
  1781  		{
  1782  			name: "blank_stage_name",
  1783  			expectedError: (&apis.FieldError{
  1784  				Message: "Stage name must contain at least one ASCII letter",
  1785  				Paths:   []string{"name"},
  1786  			}).ViaFieldIndex("stages", 0),
  1787  		},
  1788  		{
  1789  			name: "stage_name_duplicates",
  1790  			expectedError: &apis.FieldError{
  1791  				Message: "Stage names must be unique",
  1792  				Details: "The following stage names are used more than once: 'A Working Stage'",
  1793  			},
  1794  		},
  1795  		{
  1796  			name: "stage_name_duplicates_deeply_nested",
  1797  			expectedError: &apis.FieldError{
  1798  				Message: "Stage names must be unique",
  1799  				Details: "The following stage names are used more than once: 'Stage With Stages'",
  1800  			},
  1801  		},
  1802  		{
  1803  			name: "stage_name_duplicates_nested",
  1804  			expectedError: &apis.FieldError{
  1805  				Message: "Stage names must be unique",
  1806  				Details: "The following stage names are used more than once: 'Stage With Stages'",
  1807  			},
  1808  		},
  1809  		{
  1810  			name: "stage_name_duplicates_sequential",
  1811  			expectedError: &apis.FieldError{
  1812  				Message: "Stage names must be unique",
  1813  				Details: "The following stage names are used more than once: 'A Working title 2', 'A Working title'",
  1814  			},
  1815  		},
  1816  		{
  1817  			name: "stage_name_duplicates_unique_in_scope",
  1818  			expectedError: &apis.FieldError{
  1819  				Message: "Stage names must be unique",
  1820  				Details: "The following stage names are used more than once: 'A Working title 1', 'A Working title 2'",
  1821  			},
  1822  		},
  1823  		{
  1824  			name:          "loop_without_variable",
  1825  			expectedError: apis.ErrMissingField("variable").ViaField("loop").ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1826  		},
  1827  		{
  1828  			name:          "loop_without_steps",
  1829  			expectedError: apis.ErrMissingField("steps").ViaField("loop").ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1830  		},
  1831  		{
  1832  			name:          "loop_without_values",
  1833  			expectedError: apis.ErrMissingField("values").ViaField("loop").ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1834  		},
  1835  		{
  1836  			name: "top_level_container_options_with_command",
  1837  			expectedError: (&apis.FieldError{
  1838  				Message: "Command cannot be specified in containerOptions",
  1839  				Paths:   []string{"command"},
  1840  			}).ViaField("containerOptions").ViaField("options"),
  1841  		},
  1842  		{
  1843  			name:          "unknown_field",
  1844  			expectedError: errors.New("Validation failures in YAML file test_data/validation_failures/unknown_field/jenkins-x.yml:\npipelineConfig: Additional property banana is not allowed"),
  1845  		},
  1846  		{
  1847  			name: "comment_field",
  1848  			expectedError: (&apis.FieldError{
  1849  				Message: "the comment field is only valid in legacy build packs, not in jenkins-x.yml. Please remove it.",
  1850  				Paths:   []string{"comment"},
  1851  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1852  		},
  1853  		{
  1854  			name: "groovy_field",
  1855  			expectedError: (&apis.FieldError{
  1856  				Message: "the groovy field is only valid in legacy build packs, not in jenkins-x.yml. Please remove it.",
  1857  				Paths:   []string{"groovy"},
  1858  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1859  		},
  1860  		{
  1861  			name: "when_field",
  1862  			expectedError: (&apis.FieldError{
  1863  				Message: "the when field is only valid in legacy build packs, not in jenkins-x.yml. Please remove it.",
  1864  				Paths:   []string{"when"},
  1865  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1866  		},
  1867  		{
  1868  			name: "container_field",
  1869  			expectedError: (&apis.FieldError{
  1870  				Message: "the container field is deprecated - please use image instead",
  1871  				Paths:   []string{"container"},
  1872  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1873  		},
  1874  		{
  1875  			name: "legacy_steps_field",
  1876  			expectedError: (&apis.FieldError{
  1877  				Message: "the steps field is only valid in legacy build packs, not in jenkins-x.yml. Please remove it and list the nested stages sequentially instead.",
  1878  				Paths:   []string{"steps"},
  1879  			}).ViaFieldIndex("steps", 0).ViaFieldIndex("stages", 0),
  1880  		},
  1881  		{
  1882  			name: "agent_dir_field",
  1883  			expectedError: (&apis.FieldError{
  1884  				Message: "the dir field is only valid in legacy build packs, not in jenkins-x.yml. Please remove it.",
  1885  				Paths:   []string{"dir"},
  1886  			}).ViaField("agent"),
  1887  		},
  1888  		{
  1889  			name: "agent_container_field",
  1890  			expectedError: (&apis.FieldError{
  1891  				Message: "the container field is deprecated - please use image instead",
  1892  				Paths:   []string{"container"},
  1893  			}).ViaField("agent"),
  1894  		},
  1895  		{
  1896  			name: "duplicate_step_names",
  1897  			expectedError: (&apis.FieldError{
  1898  				Message: "step names within a stage must be unique",
  1899  				Details: "The following step names in the stage A Working Stage are used more than once: A Step With Spaces And Such, Another Step Name",
  1900  				Paths:   []string{"steps"},
  1901  			}).ViaFieldIndex("stages", 0),
  1902  		},
  1903  		{
  1904  			name:          "volume_missing_name",
  1905  			expectedError: apis.ErrMissingField("name").ViaFieldIndex("volumes", 0).ViaField("options"),
  1906  		},
  1907  		{
  1908  			name: "top_level_missing_volume",
  1909  			expectedError: (&apis.FieldError{
  1910  				Message: "Volume mount name not-present not found in volumes for stage or pipeline",
  1911  				Paths:   []string{"name"},
  1912  			}).ViaFieldIndex("volumeMounts", 0).ViaField("containerOptions").ViaField("options"),
  1913  		},
  1914  		{
  1915  			name: "volume_does_not_exist",
  1916  			expectedError: (&apis.FieldError{
  1917  				Message: "PVC does-not-exist does not exist, so cannot be used as a volume",
  1918  				Paths:   []string{"claimName"},
  1919  			}).ViaFieldIndex("volumes", 0).ViaField("options"),
  1920  		},
  1921  	}
  1922  
  1923  	for _, tt := range tests {
  1924  		t.Run(tt.name, func(t *testing.T) {
  1925  			projectConfig, fn, err := config.LoadProjectConfig(filepath.Join("test_data", "validation_failures", tt.name))
  1926  			if err != nil && err.Error() != tt.expectedError.Error() {
  1927  				t.Fatalf("Failed to parse YAML for %s: %q", tt.name, err)
  1928  			}
  1929  			if _, ok := tt.expectedError.(*apis.FieldError); ok {
  1930  
  1931  				if projectConfig.PipelineConfig == nil {
  1932  					t.Fatalf("PipelineConfig at %s is nil: %+v", fn, projectConfig)
  1933  				}
  1934  				if &projectConfig.PipelineConfig.Pipelines == nil {
  1935  					t.Fatalf("Pipelines at %s is nil: %+v", fn, projectConfig.PipelineConfig)
  1936  				}
  1937  				if projectConfig.PipelineConfig.Pipelines.Release == nil {
  1938  					t.Fatalf("Release at %s is nil: %+v", fn, projectConfig.PipelineConfig.Pipelines)
  1939  				}
  1940  				if projectConfig.PipelineConfig.Pipelines.Release.Pipeline == nil {
  1941  					t.Fatalf("Pipeline at %s is nil: %+v", fn, projectConfig.PipelineConfig.Pipelines.Release)
  1942  				}
  1943  				parsed := projectConfig.PipelineConfig.Pipelines.Release.Pipeline
  1944  				kubeClient := kubefake.NewSimpleClientset()
  1945  
  1946  				err = parsed.ValidateInCluster(ctx, kubeClient, "jx")
  1947  
  1948  				if err == nil {
  1949  					t.Fatalf("Expected a validation failure but none occurred")
  1950  				}
  1951  
  1952  				if d := cmp.Diff(tt.expectedError, err, cmp.AllowUnexported(apis.FieldError{})); d != "" {
  1953  					t.Fatalf("Validation error did not meet expectation: %s", d)
  1954  				}
  1955  			}
  1956  		})
  1957  	}
  1958  }
  1959  
  1960  func getOverridesTestPipeline() *syntax.ParsedPipeline {
  1961  	return sh.ParsedPipeline(
  1962  		sh.PipelineAgent("some-image"),
  1963  		sh.PipelineStage("A Working Stage",
  1964  			sh.StageStep(
  1965  				sh.StepCmd("echo"),
  1966  				sh.StepArg("hello"), sh.StepArg("world")),
  1967  		),
  1968  		sh.PipelineStage("Another stage",
  1969  			sh.StageStep(
  1970  				sh.StepCmd("echo"),
  1971  				sh.StepArg("again"))),
  1972  	)
  1973  }
  1974  
  1975  func getOverridesTestVolume() *corev1.Volume {
  1976  	return &corev1.Volume{
  1977  		Name: "stage-volume",
  1978  		VolumeSource: corev1.VolumeSource{
  1979  			PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
  1980  				ClaimName: "stage-volume",
  1981  				ReadOnly:  true,
  1982  			},
  1983  		},
  1984  	}
  1985  }
  1986  
  1987  func getOverridesTestSidecar() *corev1.Container {
  1988  	return &corev1.Container{
  1989  		Name:  "stage-sidecar",
  1990  		Image: "stage/sidecar:tag",
  1991  	}
  1992  }
  1993  
  1994  func getOverridesTestContainerOptions() *corev1.Container {
  1995  	return &corev1.Container{
  1996  		Resources: corev1.ResourceRequirements{
  1997  			Limits: corev1.ResourceList{
  1998  				"cpu":    resource.MustParse("100m"),
  1999  				"memory": resource.MustParse("128Mi"),
  2000  			},
  2001  		},
  2002  	}
  2003  }
  2004  
  2005  func TestApplyNonStepOverridesToPipeline(t *testing.T) {
  2006  	tests := []struct {
  2007  		name     string
  2008  		override *syntax.PipelineOverride
  2009  		expected *syntax.ParsedPipeline
  2010  	}{
  2011  		{
  2012  			name: "volume-on-whole-pipeline",
  2013  			override: &syntax.PipelineOverride{
  2014  				Volumes: []*corev1.Volume{getOverridesTestVolume()},
  2015  			},
  2016  			expected: sh.ParsedPipeline(
  2017  				sh.PipelineOptions(sh.PipelineVolume(getOverridesTestVolume())),
  2018  				sh.PipelineAgent("some-image"),
  2019  				sh.PipelineStage("A Working Stage",
  2020  					sh.StageStep(
  2021  						sh.StepCmd("echo"),
  2022  						sh.StepArg("hello"), sh.StepArg("world")),
  2023  				),
  2024  				sh.PipelineStage("Another stage",
  2025  					sh.StageStep(
  2026  						sh.StepCmd("echo"),
  2027  						sh.StepArg("again"))),
  2028  			),
  2029  		},
  2030  		{
  2031  			name: "volume-on-single-stage",
  2032  			override: &syntax.PipelineOverride{
  2033  				Stage:   "Another stage",
  2034  				Volumes: []*corev1.Volume{getOverridesTestVolume()},
  2035  			},
  2036  			expected: sh.ParsedPipeline(
  2037  				sh.PipelineAgent("some-image"),
  2038  				sh.PipelineStage("A Working Stage",
  2039  					sh.StageStep(
  2040  						sh.StepCmd("echo"),
  2041  						sh.StepArg("hello"), sh.StepArg("world")),
  2042  				),
  2043  				sh.PipelineStage("Another stage",
  2044  					sh.StageOptions(sh.StageVolume(getOverridesTestVolume())),
  2045  					sh.StageStep(
  2046  						sh.StepCmd("echo"),
  2047  						sh.StepArg("again"))),
  2048  			),
  2049  		},
  2050  		{
  2051  			name: "sidecar-on-whole-pipeline",
  2052  			override: &syntax.PipelineOverride{
  2053  				Sidecars: []*corev1.Container{getOverridesTestSidecar()},
  2054  			},
  2055  			expected: sh.ParsedPipeline(
  2056  				sh.PipelineOptions(sh.PipelineSidecar(getOverridesTestSidecar())),
  2057  				sh.PipelineAgent("some-image"),
  2058  				sh.PipelineStage("A Working Stage",
  2059  					sh.StageStep(
  2060  						sh.StepCmd("echo"),
  2061  						sh.StepArg("hello"), sh.StepArg("world")),
  2062  				),
  2063  				sh.PipelineStage("Another stage",
  2064  					sh.StageStep(
  2065  						sh.StepCmd("echo"),
  2066  						sh.StepArg("again"))),
  2067  			),
  2068  		},
  2069  		{
  2070  			name: "sidecar-on-single-stage",
  2071  			override: &syntax.PipelineOverride{
  2072  				Stage:    "Another stage",
  2073  				Sidecars: []*corev1.Container{getOverridesTestSidecar()},
  2074  			},
  2075  			expected: sh.ParsedPipeline(
  2076  				sh.PipelineAgent("some-image"),
  2077  				sh.PipelineStage("A Working Stage",
  2078  					sh.StageStep(
  2079  						sh.StepCmd("echo"),
  2080  						sh.StepArg("hello"), sh.StepArg("world")),
  2081  				),
  2082  				sh.PipelineStage("Another stage",
  2083  					sh.StageOptions(sh.StageSidecar(getOverridesTestSidecar())),
  2084  					sh.StageStep(
  2085  						sh.StepCmd("echo"),
  2086  						sh.StepArg("again"))),
  2087  			),
  2088  		},
  2089  		{
  2090  			name: "containerOptions-on-whole-pipeline",
  2091  			override: &syntax.PipelineOverride{
  2092  				ContainerOptions: getOverridesTestContainerOptions(),
  2093  			},
  2094  			expected: sh.ParsedPipeline(
  2095  				sh.PipelineOptions(
  2096  					sh.PipelineContainerOptions(
  2097  						sh.ContainerResourceLimits("100m", "128Mi"),
  2098  					),
  2099  				),
  2100  				sh.PipelineAgent("some-image"),
  2101  				sh.PipelineStage("A Working Stage",
  2102  					sh.StageStep(
  2103  						sh.StepCmd("echo"),
  2104  						sh.StepArg("hello"), sh.StepArg("world")),
  2105  				),
  2106  				sh.PipelineStage("Another stage",
  2107  					sh.StageStep(
  2108  						sh.StepCmd("echo"),
  2109  						sh.StepArg("again"))),
  2110  			),
  2111  		},
  2112  		{
  2113  			name: "containerOptions-on-single-stage",
  2114  			override: &syntax.PipelineOverride{
  2115  				Stage:            "Another stage",
  2116  				ContainerOptions: getOverridesTestContainerOptions(),
  2117  			},
  2118  			expected: sh.ParsedPipeline(
  2119  				sh.PipelineAgent("some-image"),
  2120  				sh.PipelineStage("A Working Stage",
  2121  					sh.StageStep(
  2122  						sh.StepCmd("echo"),
  2123  						sh.StepArg("hello"), sh.StepArg("world")),
  2124  				),
  2125  				sh.PipelineStage("Another stage",
  2126  					sh.StageOptions(
  2127  						sh.StageContainerOptions(
  2128  							sh.ContainerResourceLimits("100m", "128Mi"),
  2129  						),
  2130  					),
  2131  					sh.StageStep(
  2132  						sh.StepCmd("echo"),
  2133  						sh.StepArg("again"))),
  2134  			),
  2135  		},
  2136  		{
  2137  			name: "agent-on-whole-pipeline",
  2138  			override: &syntax.PipelineOverride{
  2139  				Agent: &syntax.Agent{
  2140  					Image: "some-other-image",
  2141  				},
  2142  			},
  2143  			expected: sh.ParsedPipeline(
  2144  				sh.PipelineAgent("some-other-image"),
  2145  				sh.PipelineStage("A Working Stage",
  2146  					sh.StageStep(
  2147  						sh.StepCmd("echo"),
  2148  						sh.StepArg("hello"), sh.StepArg("world")),
  2149  				),
  2150  				sh.PipelineStage("Another stage",
  2151  					sh.StageStep(
  2152  						sh.StepCmd("echo"),
  2153  						sh.StepArg("again"))),
  2154  			),
  2155  		},
  2156  		{
  2157  			name: "agent-on-single-stage",
  2158  			override: &syntax.PipelineOverride{
  2159  				Stage: "Another stage",
  2160  				Agent: &syntax.Agent{
  2161  					Image: "some-other-image",
  2162  				},
  2163  			},
  2164  			expected: sh.ParsedPipeline(
  2165  				sh.PipelineAgent("some-image"),
  2166  				sh.PipelineStage("A Working Stage",
  2167  					sh.StageStep(
  2168  						sh.StepCmd("echo"),
  2169  						sh.StepArg("hello"), sh.StepArg("world")),
  2170  				),
  2171  				sh.PipelineStage("Another stage",
  2172  					sh.StageAgent("some-other-image"),
  2173  					sh.StageStep(
  2174  						sh.StepCmd("echo"),
  2175  						sh.StepArg("again"))),
  2176  			),
  2177  		},
  2178  	}
  2179  
  2180  	for _, tt := range tests {
  2181  		t.Run(tt.name, func(t *testing.T) {
  2182  			newPipeline := syntax.ApplyNonStepOverridesToPipeline(getOverridesTestPipeline(), tt.override)
  2183  
  2184  			if d, _ := kmp.SafeDiff(tt.expected, newPipeline); d != "" {
  2185  				t.Errorf("Overridden pipeline did not match expected: %s", d)
  2186  			}
  2187  		})
  2188  	}
  2189  }
  2190  
  2191  func TestRfc1035LabelMangling(t *testing.T) {
  2192  	tests := []struct {
  2193  		name     string
  2194  		input    string
  2195  		expected string
  2196  	}{
  2197  		{
  2198  			name:     "unmodified",
  2199  			input:    "unmodified",
  2200  			expected: "unmodified-suffix",
  2201  		},
  2202  		{
  2203  			name:     "spaces",
  2204  			input:    "A Simple Test.",
  2205  			expected: "a-simple-test-suffix",
  2206  		},
  2207  		{
  2208  			name:     "no leading digits",
  2209  			input:    "0123456789no-leading-digits",
  2210  			expected: "no-leading-digits-suffix",
  2211  		},
  2212  		{
  2213  			name:     "no leading hyphens",
  2214  			input:    "----no-leading-hyphens",
  2215  			expected: "no-leading-hyphens-suffix",
  2216  		},
  2217  		{
  2218  			name:     "no consecutive hyphens",
  2219  			input:    "no--consecutive- hyphens",
  2220  			expected: "no-consecutive-hyphens-suffix",
  2221  		},
  2222  		{
  2223  			name:     "no trailing hyphens",
  2224  			input:    "no-trailing-hyphens----",
  2225  			expected: "no-trailing-hyphens-suffix",
  2226  		},
  2227  		{
  2228  			name:     "no symbols",
  2229  			input:    "&$^#@(*&$^-whoops",
  2230  			expected: "whoops-suffix",
  2231  		},
  2232  		{
  2233  			name:     "no unprintable characters",
  2234  			input:    "a\n\t\x00b",
  2235  			expected: "ab-suffix",
  2236  		},
  2237  		{
  2238  			name:     "no unicode",
  2239  			input:    "japan-日本",
  2240  			expected: "japan-suffix",
  2241  		},
  2242  		{
  2243  			name:     "no non-bmp characters",
  2244  			input:    "happy 😃",
  2245  			expected: "happy-suffix",
  2246  		},
  2247  		{
  2248  			name:     "truncated to 63",
  2249  			input:    "a0123456789012345678901234567890123456789012345678901234567890123456789",
  2250  			expected: "a0123456789012345678901234567890123456789012345678901234-suffix",
  2251  		},
  2252  		{
  2253  			name:     "truncated to 62",
  2254  			input:    "a012345678901234567890123456789012345678901234567890123-567890123456789",
  2255  			expected: "a012345678901234567890123456789012345678901234567890123-suffix",
  2256  		},
  2257  	}
  2258  
  2259  	for _, tt := range tests {
  2260  		t.Run(tt.name, func(t *testing.T) {
  2261  			mangled := syntax.MangleToRfc1035Label(tt.input, "suffix")
  2262  			if d := cmp.Diff(tt.expected, mangled); d != "" {
  2263  				t.Fatalf("Mangled output did not match expected output: %s", d)
  2264  			}
  2265  		})
  2266  	}
  2267  }
  2268  
  2269  func TestParsedPipelineHelpers(t *testing.T) {
  2270  	input := sh.ParsedPipeline(
  2271  		sh.PipelineAgent("some-image"),
  2272  		sh.PipelineOptions(
  2273  			sh.PipelineOptionsRetry(5),
  2274  			sh.PipelineOptionsTimeout(30, syntax.TimeoutUnitSeconds),
  2275  			sh.PipelineSidecar(&corev1.Container{Name: "my-sidecar", Image: "banana:latest"}),
  2276  			sh.PipelineVolume(&corev1.Volume{Name: "banana"}),
  2277  		),
  2278  		sh.PipelineEnvVar("ANIMAL", "MONKEY"),
  2279  		sh.PipelineEnvVar("FRUIT", "BANANA"),
  2280  		sh.PipelinePost(syntax.PostConditionSuccess,
  2281  			sh.PostAction("mail", map[string]string{
  2282  				"to":      "foo@bar.com",
  2283  				"subject": "Yay, it passed",
  2284  			})),
  2285  		sh.PipelinePost(syntax.PostConditionFailure,
  2286  			sh.PostAction("slack", map[string]string{
  2287  				"whatever": "the",
  2288  				"slack":    "config",
  2289  				"actually": "is. =)",
  2290  			})),
  2291  		sh.PipelineStage("A Working Stage",
  2292  			sh.StageOptions(
  2293  				sh.StageOptionsWorkspace(customWorkspace),
  2294  				sh.StageOptionsStash("some-name", "**/*"),
  2295  				sh.StageOptionsUnstash("some-name", ""),
  2296  				sh.StageOptionsTimeout(15, syntax.TimeoutUnitMinutes),
  2297  				sh.StageOptionsRetry(2),
  2298  				sh.StageSidecar(&corev1.Container{Name: "some-sidecar", Image: "some/sidecar:tag"}),
  2299  				sh.StageVolume(&corev1.Volume{Name: "apple"}),
  2300  				sh.StageVolume(&corev1.Volume{Name: "orange"}),
  2301  			),
  2302  			sh.StageStep(
  2303  				sh.StepCmd("echo"),
  2304  				sh.StepArg("hello"),
  2305  				sh.StepArg("world"),
  2306  			),
  2307  		),
  2308  		sh.PipelineStage("Parent Stage",
  2309  			sh.StageParallel("First Nested Stage",
  2310  				sh.StageAgent("some-other-image"),
  2311  				sh.StageStep(
  2312  					sh.StepCmd("echo"),
  2313  					sh.StepArg("hello"),
  2314  					sh.StepArg("world"),
  2315  					sh.StepAgent("some-other-image"),
  2316  				),
  2317  				sh.StageEnvVar("STAGE_VAR_ONE", "some value"),
  2318  				sh.StageEnvVar("STAGE_VAR_TWO", "some other value"),
  2319  				sh.StagePost(syntax.PostConditionAlways,
  2320  					sh.PostAction("junit", map[string]string{
  2321  						"pattern": "target/surefire-reports/**/*.xml",
  2322  					}),
  2323  				),
  2324  			),
  2325  			sh.StageParallel("Nested In Parallel",
  2326  				sh.StageSequential("Another stage",
  2327  					sh.StageStep(
  2328  						sh.StepLoop("SOME_VAR", []string{"a", "b", "c"},
  2329  							sh.LoopStep(
  2330  								sh.StepCmd("echo"),
  2331  								sh.StepArg("SOME_VAR is ${SOME_VAR}"),
  2332  							),
  2333  						),
  2334  					),
  2335  				),
  2336  				sh.StageSequential("Some other stage",
  2337  					sh.StageStep(
  2338  						sh.StepCmd("echo"),
  2339  						sh.StepArg("otherwise"),
  2340  						sh.StepDir(customWorkspace),
  2341  					),
  2342  					sh.StageStep(
  2343  						sh.StepStep("some-step"),
  2344  						sh.StepOptions(map[string]string{"first": "arg", "second": "arg"}),
  2345  					),
  2346  				),
  2347  			),
  2348  		),
  2349  	)
  2350  
  2351  	expected := &syntax.ParsedPipeline{
  2352  		Agent: &syntax.Agent{
  2353  			Image: "some-image",
  2354  		},
  2355  		Options: &syntax.RootOptions{
  2356  			Retry: 5,
  2357  			Timeout: &syntax.Timeout{
  2358  				Time: 30,
  2359  				Unit: syntax.TimeoutUnitSeconds,
  2360  			},
  2361  			Sidecars: []*corev1.Container{{
  2362  				Name:  "my-sidecar",
  2363  				Image: "banana:latest",
  2364  			}},
  2365  			Volumes: []*corev1.Volume{{
  2366  				Name: "banana",
  2367  			}},
  2368  		},
  2369  		Env: []corev1.EnvVar{
  2370  			{
  2371  				Name:  "ANIMAL",
  2372  				Value: "MONKEY",
  2373  			},
  2374  			{
  2375  				Name:  "FRUIT",
  2376  				Value: "BANANA",
  2377  			},
  2378  		},
  2379  		Post: []syntax.Post{
  2380  			{
  2381  				Condition: "success",
  2382  				Actions: []syntax.PostAction{{
  2383  					Name: "mail",
  2384  					Options: map[string]string{
  2385  						"to":      "foo@bar.com",
  2386  						"subject": "Yay, it passed",
  2387  					},
  2388  				}},
  2389  			},
  2390  			{
  2391  				Condition: "failure",
  2392  				Actions: []syntax.PostAction{{
  2393  					Name: "slack",
  2394  					Options: map[string]string{
  2395  						"whatever": "the",
  2396  						"slack":    "config",
  2397  						"actually": "is. =)",
  2398  					},
  2399  				}},
  2400  			},
  2401  		},
  2402  		Stages: []syntax.Stage{
  2403  			{
  2404  				Name: "A Working Stage",
  2405  				Options: &syntax.StageOptions{
  2406  					Workspace: &customWorkspace,
  2407  					Stash: &syntax.Stash{
  2408  						Name:  "some-name",
  2409  						Files: "**/*",
  2410  					},
  2411  					Unstash: &syntax.Unstash{
  2412  						Name: "some-name",
  2413  					},
  2414  					RootOptions: &syntax.RootOptions{
  2415  						Timeout: &syntax.Timeout{
  2416  							Time: 15,
  2417  							Unit: syntax.TimeoutUnitMinutes,
  2418  						},
  2419  						Retry: 2,
  2420  						Sidecars: []*corev1.Container{{
  2421  							Name:  "some-sidecar",
  2422  							Image: "some/sidecar:tag",
  2423  						}},
  2424  						Volumes: []*corev1.Volume{{
  2425  							Name: "apple",
  2426  						}, {
  2427  							Name: "orange",
  2428  						}},
  2429  					},
  2430  				},
  2431  				Steps: []syntax.Step{{
  2432  					Command:   "echo",
  2433  					Arguments: []string{"hello", "world"},
  2434  				}},
  2435  			},
  2436  			{
  2437  				Name: "Parent Stage",
  2438  				Parallel: []syntax.Stage{
  2439  					{
  2440  						Name: "First Nested Stage",
  2441  						Agent: &syntax.Agent{
  2442  							Image: "some-other-image",
  2443  						},
  2444  						Steps: []syntax.Step{{
  2445  							Command:   "echo",
  2446  							Arguments: []string{"hello", "world"},
  2447  							Agent: &syntax.Agent{
  2448  								Image: "some-other-image",
  2449  							},
  2450  						}},
  2451  						Env: []corev1.EnvVar{
  2452  							{
  2453  								Name:  "STAGE_VAR_ONE",
  2454  								Value: "some value",
  2455  							},
  2456  							{
  2457  								Name:  "STAGE_VAR_TWO",
  2458  								Value: "some other value",
  2459  							},
  2460  						},
  2461  						Post: []syntax.Post{{
  2462  							Condition: "always",
  2463  							Actions: []syntax.PostAction{{
  2464  								Name: "junit",
  2465  								Options: map[string]string{
  2466  									"pattern": "target/surefire-reports/**/*.xml",
  2467  								},
  2468  							}},
  2469  						}},
  2470  					},
  2471  					{
  2472  						Name: "Nested In Parallel",
  2473  						Stages: []syntax.Stage{
  2474  							{
  2475  								Name: "Another stage",
  2476  								Steps: []syntax.Step{{
  2477  									Loop: &syntax.Loop{
  2478  										Variable: "SOME_VAR",
  2479  										Values:   []string{"a", "b", "c"},
  2480  										Steps: []syntax.Step{{
  2481  											Command:   "echo",
  2482  											Arguments: []string{"SOME_VAR is ${SOME_VAR}"},
  2483  										}},
  2484  									},
  2485  								}},
  2486  							},
  2487  							{
  2488  								Name: "Some other stage",
  2489  								Steps: []syntax.Step{
  2490  									{
  2491  										Command:   "echo",
  2492  										Arguments: []string{"otherwise"},
  2493  										Dir:       customWorkspace,
  2494  									},
  2495  									{
  2496  										Step:    "some-step",
  2497  										Options: map[string]string{"first": "arg", "second": "arg"},
  2498  									},
  2499  								},
  2500  							},
  2501  						},
  2502  					},
  2503  				},
  2504  			},
  2505  		},
  2506  	}
  2507  
  2508  	if d := cmp.Diff(expected, input); d != "" {
  2509  		t.Fatalf("ParsedPipeline diff -want, +got: %v", d)
  2510  	}
  2511  }