From 6713f829e4763c69ddc31d4eb2783521c1b5f87a Mon Sep 17 00:00:00 2001 From: rhysd Date: Wed, 21 Sep 2022 09:34:36 +0900 Subject: [PATCH 1/5] fix input/output/secret names of reusable workflow are case-insensitive (#216) --- ast.go | 6 +- parse.go | 4 +- reusable_workflow.go | 137 +++++++++++++++++++++++++++++++------ reusable_workflow_test.go | 125 +++++++++++++++------------------ rule_expression.go | 4 +- rule_workflow_call.go | 12 ++-- rule_workflow_call_test.go | 31 ++++----- 7 files changed, 201 insertions(+), 118 deletions(-) diff --git a/ast.go b/ast.go index a7645d6d9..b1e01677f 100644 --- a/ast.go +++ b/ast.go @@ -762,9 +762,11 @@ type WorkflowCallSecret struct { type WorkflowCall struct { // Uses is a workflow specification to be called. This field is mandatory. Uses *String - // Inputs is a map from input name to input value at 'with:'. + // Inputs is a map from input name to input value at 'with:'. Keys are in lower case since input names + // are case-insensitive. Inputs map[string]*WorkflowCallInput - // Secrets is a map from secret name to secret value at 'secrets:'. + // Secrets is a map from secret name to secret value at 'secrets:'. Keys are in lower case since input + // names are case-insensitive. Secrets map[string]*WorkflowCallSecret // InheritSecrets is true when 'secrets: inherit' is specified. In this case, Secrets must be empty. // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callsecretsinherit diff --git a/parse.go b/parse.go index 427b0dfe3..f788f9565 100644 --- a/parse.go +++ b/parse.go @@ -1159,7 +1159,7 @@ func (p *parser) parseJob(id *String, n *yaml.Node) *Job { with := p.parseSectionMapping("with", v, false) call.Inputs = make(map[string]*WorkflowCallInput, len(with)) for _, i := range with { - call.Inputs[i.key.Value] = &WorkflowCallInput{ + call.Inputs[strings.ToLower(i.key.Value)] = &WorkflowCallInput{ Name: i.key, Value: p.parseString(i.val, true), } @@ -1178,7 +1178,7 @@ func (p *parser) parseJob(id *String, n *yaml.Node) *Job { secrets := p.parseSectionMapping("secrets", v, false) call.Secrets = make(map[string]*WorkflowCallSecret, len(secrets)) for _, s := range secrets { - call.Secrets[s.key.Value] = &WorkflowCallSecret{ + call.Secrets[strings.ToLower(s.key.Value)] = &WorkflowCallSecret{ Name: s.key, Value: p.parseString(s.val, true), } diff --git a/reusable_workflow.go b/reusable_workflow.go index 018a6b5ed..ed4ef5324 100644 --- a/reusable_workflow.go +++ b/reusable_workflow.go @@ -11,8 +11,10 @@ import ( "gopkg.in/yaml.v3" ) -// ReusableWorkflowMetadataInput is input metadata for validating local reusable workflow file. +// ReusableWorkflowMetadataInput is an input metadata for validating local reusable workflow file. type ReusableWorkflowMetadataInput struct { + // Name is a name of the input defined in the reusable workflow. + Name string // Required is true when 'required' field of the input is set to true and no default value is set. Required bool // Type is a type of the input. When the input type is unknown, 'any' type is set. @@ -47,23 +49,112 @@ func (input *ReusableWorkflowMetadataInput) UnmarshalYAML(n *yaml.Node) error { return nil } -// ReusableWorkflowMetadataSecretRequired is metadata to indicate a secret of reusable workflow is -// required or not. -type ReusableWorkflowMetadataSecretRequired bool +// ReusableWorkflowMetadataInputs is a map from input name to reusable wokflow input metadata. The +// keys are in lower case since input names of workflow calls are case insensitive. +type ReusableWorkflowMetadataInputs map[string]*ReusableWorkflowMetadataInput // UnmarshalYAML implements yaml.Unmarshaler. -func (required *ReusableWorkflowMetadataSecretRequired) UnmarshalYAML(n *yaml.Node) error { - type metadata struct { - Required bool `yaml:"required"` +func (inputs *ReusableWorkflowMetadataInputs) UnmarshalYAML(n *yaml.Node) error { + if n.Kind != yaml.MappingNode { + return fmt.Errorf( + "yaml: on.workflow_call.inputs must be mapping node but found %s node at line:%d, col:%d", + nodeKindName(n.Kind), + n.Line, + n.Column, + ) } - var md metadata - if err := n.Decode(&md); err != nil { - return err + md := make(ReusableWorkflowMetadataInputs, len(n.Content)/2) + for i := 0; i < len(n.Content); i += 2 { + k, v := n.Content[i], n.Content[i+1] + + var m ReusableWorkflowMetadataInput + if err := v.Decode(&m); err != nil { + return err + } + m.Name = k.Value + if m.Type == nil { + m.Type = AnyType{} // Reach here when `v` is null node + } + + md[strings.ToLower(k.Value)] = &m + } + + *inputs = md + return nil +} + +// ReusableWorkflowMetadataSecret is a secret metadata for validating local reusable workflow file. +type ReusableWorkflowMetadataSecret struct { + // Name is a name of the secret in the reusable workflow. + Name string + // Required indicates wether the secret is required by its reusable workflow. When this value is + // true, workflow calls must set this secret unless secrets are not inherited. + Required bool `yaml:"required"` +} + +// ReusableWorkflowMetadataSecrets is a map from secret name to reusable wokflow secret metadata. +// The keys are in lower case since secret names of workflow calls are case insensitive. +type ReusableWorkflowMetadataSecrets map[string]*ReusableWorkflowMetadataSecret + +// UnmarshalYAML implements yaml.Unmarshaler. +func (secrets *ReusableWorkflowMetadataSecrets) UnmarshalYAML(n *yaml.Node) error { + if n.Kind != yaml.MappingNode { + return fmt.Errorf( + "yaml: on.workflow_call.secrets must be mapping node but found %s node at line:%d, col:%d", + nodeKindName(n.Kind), + n.Line, + n.Column, + ) } - *required = ReusableWorkflowMetadataSecretRequired(md.Required) + md := make(ReusableWorkflowMetadataSecrets, len(n.Content)/2) + for i := 0; i < len(n.Content); i += 2 { + k, v := n.Content[i], n.Content[i+1] + var s ReusableWorkflowMetadataSecret + if err := v.Decode(&s); err != nil { + return err + } + s.Name = k.Value + + md[strings.ToLower(k.Value)] = &s + } + + *secrets = md + return nil +} + +// ReusableWorkflowMetadataOutput is an output metadata for validating local reusable workflow file. +type ReusableWorkflowMetadataOutput struct { + // Name is a name of the output in the reusable workflow. + Name string +} + +// ReusableWorkflowMetadataOutputs is a map from output name to reusable wokflow output metadata. +// The keys are in lower case since output names of workflow calls are case insensitive. +type ReusableWorkflowMetadataOutputs map[string]*ReusableWorkflowMetadataOutput + +// UnmarshalYAML implements yaml.Unmarshaler. +func (outputs *ReusableWorkflowMetadataOutputs) UnmarshalYAML(n *yaml.Node) error { + if n.Kind != yaml.MappingNode { + return fmt.Errorf( + "yaml: on.workflow_call.outputs must be mapping node but found %s node at line:%d, col:%d", + nodeKindName(n.Kind), + n.Line, + n.Column, + ) + } + + md := make(ReusableWorkflowMetadataOutputs, len(n.Content)) + for i := 0; i < len(n.Content); i += 2 { + k := n.Content[i] + md[strings.ToLower(k.Value)] = &ReusableWorkflowMetadataOutput{ + Name: k.Value, + } + } + + *outputs = md return nil } @@ -71,9 +162,9 @@ func (required *ReusableWorkflowMetadataSecretRequired) UnmarshalYAML(n *yaml.No // contain all metadata from YAML file. It only contains metadata which is necessary to validate // reusable workflow files by actionlint. type ReusableWorkflowMetadata struct { - Inputs map[string]*ReusableWorkflowMetadataInput `yaml:"inputs"` - Outputs map[string]struct{} `yaml:"outputs"` - Secrets map[string]ReusableWorkflowMetadataSecretRequired `yaml:"secrets"` + Inputs ReusableWorkflowMetadataInputs `yaml:"inputs"` + Outputs ReusableWorkflowMetadataOutputs `yaml:"outputs"` + Secrets ReusableWorkflowMetadataSecrets `yaml:"secrets"` } // LocalReusableWorkflowCache is a cache for local reusable workflow metadata files. It avoids find/read/parse @@ -191,9 +282,9 @@ func (c *LocalReusableWorkflowCache) WriteWorkflowCallEvent(wpath string, event } m := &ReusableWorkflowMetadata{ - Inputs: map[string]*ReusableWorkflowMetadataInput{}, - Outputs: map[string]struct{}{}, - Secrets: map[string]ReusableWorkflowMetadataSecretRequired{}, + Inputs: ReusableWorkflowMetadataInputs{}, + Outputs: ReusableWorkflowMetadataOutputs{}, + Secrets: ReusableWorkflowMetadataSecrets{}, } for n, i := range event.Inputs { @@ -206,19 +297,25 @@ func (c *LocalReusableWorkflowCache) WriteWorkflowCallEvent(wpath string, event case WorkflowCallEventInputTypeString: t = StringType{} } - m.Inputs[n.Value] = &ReusableWorkflowMetadataInput{ + m.Inputs[strings.ToLower(n.Value)] = &ReusableWorkflowMetadataInput{ Type: t, Required: i.Required != nil && i.Required.Value && i.Default == nil, + Name: n.Value, } } for n := range event.Outputs { - m.Outputs[n.Value] = struct{}{} + m.Outputs[strings.ToLower(n.Value)] = &ReusableWorkflowMetadataOutput{ + Name: n.Value, + } } for n, s := range event.Secrets { r := s.Required != nil && s.Required.Value - m.Secrets[n.Value] = ReusableWorkflowMetadataSecretRequired(r) + m.Secrets[strings.ToLower(n.Value)] = &ReusableWorkflowMetadataSecret{ + Required: r, + Name: n.Value, + } } c.mu.Lock() diff --git a/reusable_workflow_test.go b/reusable_workflow_test.go index c517fc9bc..d989b9788 100644 --- a/reusable_workflow_test.go +++ b/reusable_workflow_test.go @@ -30,16 +30,14 @@ func TestReusableWorkflowUnmarshalOK(t *testing.T) { x: `, want: &ReusableWorkflowMetadata{ - Inputs: map[string]*ReusableWorkflowMetadataInput{ - "i": { - Type: StringType{}, - }, + Inputs: ReusableWorkflowMetadataInputs{ + "i": {"i", false, StringType{}}, }, - Outputs: map[string]struct{}{ - "o": {}, + Outputs: ReusableWorkflowMetadataOutputs{ + "o": {"o"}, }, - Secrets: map[string]ReusableWorkflowMetadataSecretRequired{ - "x": false, + Secrets: ReusableWorkflowMetadataSecrets{ + "x": {"x", false}, }, }, }, @@ -96,32 +94,20 @@ func TestReusableWorkflowUnmarshalOK(t *testing.T) { type: string default: abc requried: true + i: + required: true `, want: &ReusableWorkflowMetadata{ - Inputs: map[string]*ReusableWorkflowMetadataInput{ - "a": { - Type: StringType{}, - }, - "b": { - Type: NumberType{}, - }, - "c": { - Type: BoolType{}, - }, - "d": nil, - "e": { - Type: StringType{}, - }, - "f": { - Type: StringType{}, - Required: true, - }, - "g": { - Type: StringType{}, - }, - "h": { - Type: StringType{}, - }, + Inputs: ReusableWorkflowMetadataInputs{ + "a": {"a", false, StringType{}}, + "b": {"b", false, NumberType{}}, + "c": {"c", false, BoolType{}}, + "d": {"d", false, AnyType{}}, + "e": {"e", false, StringType{}}, + "f": {"f", true, StringType{}}, + "g": {"g", false, StringType{}}, + "h": {"h", false, StringType{}}, + "i": {"i", true, AnyType{}}, }, Outputs: nil, Secrets: nil, @@ -142,16 +128,14 @@ func TestReusableWorkflowUnmarshalOK(t *testing.T) { x: `, want: &ReusableWorkflowMetadata{ - Inputs: map[string]*ReusableWorkflowMetadataInput{ - "i": { - Type: StringType{}, - }, + Inputs: ReusableWorkflowMetadataInputs{ + "i": {"i", false, StringType{}}, }, - Outputs: map[string]struct{}{ - "o": {}, + Outputs: ReusableWorkflowMetadataOutputs{ + "o": {"o"}, }, - Secrets: map[string]ReusableWorkflowMetadataSecretRequired{ - "x": false, + Secrets: ReusableWorkflowMetadataSecrets{ + "x": {"x", false}, }, }, }, @@ -170,10 +154,10 @@ func TestReusableWorkflowUnmarshalOK(t *testing.T) { want: &ReusableWorkflowMetadata{ Inputs: nil, Outputs: nil, - Secrets: map[string]ReusableWorkflowMetadataSecretRequired{ - "x": false, - "y": false, - "z": true, + Secrets: ReusableWorkflowMetadataSecrets{ + "x": {"x", false}, + "y": {"y", false}, + "z": {"z", true}, }, }, }, @@ -322,16 +306,16 @@ func TestReusableWorkflowUnmarshalEventNotFound(t *testing.T) { } var testReusableWorkflowWantedMetadata *ReusableWorkflowMetadata = &ReusableWorkflowMetadata{ - Inputs: map[string]*ReusableWorkflowMetadataInput{ - "input1": {Type: StringType{}}, - "input2": {Type: BoolType{}, Required: true}, + Inputs: ReusableWorkflowMetadataInputs{ + "input1": {"input1", false, StringType{}}, + "input2": {"input2", true, BoolType{}}, }, - Outputs: map[string]struct{}{ - "output1": {}, + Outputs: ReusableWorkflowMetadataOutputs{ + "output1": {"output1"}, }, - Secrets: map[string]ReusableWorkflowMetadataSecretRequired{ - "secret1": false, - "secret2": true, + Secrets: ReusableWorkflowMetadataSecrets{ + "secret1": {"secret1", false}, + "secret2": {"secret2", true}, }, } @@ -524,7 +508,7 @@ func TestReusableWorkflowMetadataFromASTNodeInputs(t *testing.T) { tests := []struct { what string inputs map[string]*WorkflowCallEventInput - want map[string]*ReusableWorkflowMetadataInput + want ReusableWorkflowMetadataInputs }{ { what: "type of inputs", @@ -534,11 +518,11 @@ func TestReusableWorkflowMetadataFromASTNodeInputs(t *testing.T) { "bool_input": {Type: WorkflowCallEventInputTypeBoolean}, "unknown_input": {}, }, - want: map[string]*ReusableWorkflowMetadataInput{ - "string_input": {Type: StringType{}}, - "number_input": {Type: NumberType{}}, - "bool_input": {Type: BoolType{}}, - "unknown_input": {Type: AnyType{}}, + want: ReusableWorkflowMetadataInputs{ + "string_input": {"string_input", false, StringType{}}, + "number_input": {"number_input", false, NumberType{}}, + "bool_input": {"bool_input", false, BoolType{}}, + "unknown_input": {"unknown_input", false, AnyType{}}, }, }, { @@ -558,18 +542,18 @@ func TestReusableWorkflowMetadataFromASTNodeInputs(t *testing.T) { }, }, }, - want: map[string]*ReusableWorkflowMetadataInput{ - "unspecified": {Required: false, Type: AnyType{}}, - "not_required": {Required: false, Type: AnyType{}}, - "required": {Required: true, Type: AnyType{}}, - "required_but_default": {Required: false, Type: AnyType{}}, - "expression": {Required: false, Type: AnyType{}}, + want: ReusableWorkflowMetadataInputs{ + "unspecified": {"unspecified", false, AnyType{}}, + "not_required": {"not_required", false, AnyType{}}, + "required": {"required", true, AnyType{}}, + "required_but_default": {"required_but_default", false, AnyType{}}, + "expression": {"expression", false, AnyType{}}, }, }, { what: "empty", inputs: map[string]*WorkflowCallEventInput{}, - want: map[string]*ReusableWorkflowMetadataInput{}, + want: ReusableWorkflowMetadataInputs{}, }, } @@ -626,9 +610,9 @@ func TestReusableWorkflowMetadataFromASTNodeOutputs(t *testing.T) { t.Fatal("Event was not converted to event") } - want := map[string]struct{}{} + want := ReusableWorkflowMetadataOutputs{} for _, o := range outputs { - want[o] = struct{}{} + want[o] = &ReusableWorkflowMetadataOutput{o} } if !cmp.Equal(m.Outputs, want) { @@ -674,9 +658,12 @@ func TestReusableWorkflowMetadataFromASTNodeSecrets(t *testing.T) { t.Fatal("Event was not converted to event") } - want := map[string]ReusableWorkflowMetadataSecretRequired{} + want := ReusableWorkflowMetadataSecrets{} for n, r := range secrets { - want[n] = ReusableWorkflowMetadataSecretRequired(r != nil && r.Value) + want[n] = &ReusableWorkflowMetadataSecret{ + Name: n, + Required: r != nil && r.Value, + } } if !cmp.Equal(m.Secrets, want) { diff --git a/rule_expression.go b/rule_expression.go index 8ff3d075e..aaacca3e1 100644 --- a/rule_expression.go +++ b/rule_expression.go @@ -546,7 +546,7 @@ func (rule *RuleExpression) checkWorkflowCall(c *WorkflowCall) { rule.errorf( i.Value.Pos, "input %q is typed as %s by reusable workflow %q. %s value cannot be assigned", - n, + mi.Name, mi.Type.String(), c.Uses.Value, ty.String(), @@ -999,7 +999,7 @@ func (rule *RuleExpression) checkWorkflowCallOutputs(outputs map[*String]*Workfl } else { p := make(map[string]ExprType, len(j.Outputs)) for n := range j.Outputs { - p[n] = StringType{} + p[strings.ToLower(n)] = StringType{} } o = NewStrictObjectType(p) } diff --git a/rule_workflow_call.go b/rule_workflow_call.go index fe1a0bf9a..45c49c8b1 100644 --- a/rule_workflow_call.go +++ b/rule_workflow_call.go @@ -93,7 +93,7 @@ func (rule *RuleWorkflowCall) checkWorkflowCallUsesLocal(call *WorkflowCall) { for n, i := range m.Inputs { if i != nil && i.Required { if _, ok := call.Inputs[n]; !ok { - rule.errorf(u.Pos, "input %q is required by %q reusable workflow", n, u.Value) + rule.errorf(u.Pos, "input %q is required by %q reusable workflow", i.Name, u.Value) } } } @@ -111,16 +111,16 @@ func (rule *RuleWorkflowCall) checkWorkflowCallUsesLocal(call *WorkflowCall) { note = "defined inputs are " + sortedQuotes(i) } } - rule.errorf(i.Name.Pos, "input %q is not defined in %q reusable workflow. %s", n, u.Value, note) + rule.errorf(i.Name.Pos, "input %q is not defined in %q reusable workflow. %s", i.Name.Value, u.Value, note) } } // Validate secrets if !call.InheritSecrets { - for n, r := range m.Secrets { - if r { + for n, s := range m.Secrets { + if s.Required { if _, ok := call.Secrets[n]; !ok { - rule.errorf(u.Pos, "secret %q is required by %q reusable workflow", n, u.Value) + rule.errorf(u.Pos, "secret %q is required by %q reusable workflow", s.Name, u.Value) } } } @@ -138,7 +138,7 @@ func (rule *RuleWorkflowCall) checkWorkflowCallUsesLocal(call *WorkflowCall) { note = "defined secrets are " + sortedQuotes(s) } } - rule.errorf(s.Name.Pos, "secret %q is not defined in %q reusable workflow. %s", n, u.Value, note) + rule.errorf(s.Name.Pos, "secret %q is not defined in %q reusable workflow. %s", s.Name.Value, u.Value, note) } } } diff --git a/rule_workflow_call_test.go b/rule_workflow_call_test.go index f81025e8e..4f3022eee 100644 --- a/rule_workflow_call_test.go +++ b/rule_workflow_call_test.go @@ -136,14 +136,14 @@ func TestRuleWorkflowCallWriteEventNodeToMetadataCache(t *testing.T) { } want := &ReusableWorkflowMetadata{ - Inputs: map[string]*ReusableWorkflowMetadataInput{ - "input1": {Type: StringType{}}, + Inputs: ReusableWorkflowMetadataInputs{ + "input1": {"input1", false, StringType{}}, }, - Outputs: map[string]struct{}{ - "output1": {}, + Outputs: ReusableWorkflowMetadataOutputs{ + "output1": {"output1"}, }, - Secrets: map[string]ReusableWorkflowMetadataSecretRequired{ - "secret1": false, + Secrets: ReusableWorkflowMetadataSecrets{ + "secret1": {"secret1", false}, }, } @@ -154,19 +154,16 @@ func TestRuleWorkflowCallWriteEventNodeToMetadataCache(t *testing.T) { func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { metadata := &ReusableWorkflowMetadata{ - Inputs: map[string]*ReusableWorkflowMetadataInput{ - "optional_input": {Type: StringType{}}, - "required_input": { - Type: StringType{}, - Required: true, - }, + Inputs: ReusableWorkflowMetadataInputs{ + "optional_input": {"optional_input", false, StringType{}}, + "required_input": {"required_input", true, StringType{}}, }, - Outputs: map[string]struct{}{ - "output": {}, + Outputs: ReusableWorkflowMetadataOutputs{ + "output": {"output"}, }, - Secrets: map[string]ReusableWorkflowMetadataSecretRequired{ - "optional_secret": false, - "required_secret": true, + Secrets: ReusableWorkflowMetadataSecrets{ + "optional_secret": {"optional_secret", false}, + "required_secret": {"required_secret", true}, }, } cwd := filepath.Join("testdata", "reusable_workflow_metadata") From ee1012a6049ca1c99bc844fd79e837879a359b89 Mon Sep 17 00:00:00 2001 From: rhysd Date: Wed, 21 Sep 2022 22:08:11 +0900 Subject: [PATCH 2/5] add tests for checking inputs/secrets of reusable workflow call in case-insensitive --- reusable_workflow_test.go | 54 +++++++++++++++++++++- rule_workflow_call_test.go | 92 +++++++++++++++++++++++++++++--------- 2 files changed, 122 insertions(+), 24 deletions(-) diff --git a/reusable_workflow_test.go b/reusable_workflow_test.go index d989b9788..5df2bfa31 100644 --- a/reusable_workflow_test.go +++ b/reusable_workflow_test.go @@ -194,6 +194,41 @@ func TestReusableWorkflowUnmarshalOK(t *testing.T) { Secrets: nil, }, }, + { + what: "upper case", + src: ` + on: + workflow_call: + inputs: + MY_INPUT1: + type: string + MY_INPUT2: + type: number + outputs: + MY_OUTPUT1: + value: foo + MY_OUTPUT2: + value: foo + secrets: + MY_SECRET1: + MY_SECRET2: + required: true + `, + want: &ReusableWorkflowMetadata{ + Inputs: ReusableWorkflowMetadataInputs{ + "my_input1": {"MY_INPUT1", false, StringType{}}, + "my_input2": {"MY_INPUT2", false, NumberType{}}, + }, + Outputs: ReusableWorkflowMetadataOutputs{ + "my_output1": {"MY_OUTPUT1"}, + "my_output2": {"MY_OUTPUT2"}, + }, + Secrets: ReusableWorkflowMetadataSecrets{ + "my_secret1": {"MY_SECRET1", false}, + "my_secret2": {"MY_SECRET2", true}, + }, + }, + }, } for _, tc := range tests { @@ -555,6 +590,15 @@ func TestReusableWorkflowMetadataFromASTNodeInputs(t *testing.T) { inputs: map[string]*WorkflowCallEventInput{}, want: ReusableWorkflowMetadataInputs{}, }, + { + what: "upper case input", + inputs: map[string]*WorkflowCallEventInput{ + "MY_INPUT": {Type: WorkflowCallEventInputTypeString}, + }, + want: ReusableWorkflowMetadataInputs{ + "my_input": {"MY_INPUT", false, StringType{}}, + }, + }, } for _, tc := range tests { @@ -592,6 +636,7 @@ func TestReusableWorkflowMetadataFromASTNodeOutputs(t *testing.T) { {}, {"foo"}, {"a", "b", "c"}, + {"A", "B", "C"}, } for _, outputs := range tests { t.Run(fmt.Sprintf("%s", outputs), func(t *testing.T) { @@ -612,7 +657,7 @@ func TestReusableWorkflowMetadataFromASTNodeOutputs(t *testing.T) { want := ReusableWorkflowMetadataOutputs{} for _, o := range outputs { - want[o] = &ReusableWorkflowMetadataOutput{o} + want[strings.ToLower(o)] = &ReusableWorkflowMetadataOutput{o} } if !cmp.Equal(m.Outputs, want) { @@ -640,6 +685,11 @@ func TestReusableWorkflowMetadataFromASTNodeSecrets(t *testing.T) { "b": &Bool{Value: true, Pos: &Pos{}}, "c": nil, }, + { + "A": &Bool{Value: false, Pos: &Pos{}}, + "B": &Bool{Value: true, Pos: &Pos{}}, + "C": nil, + }, } for _, secrets := range tests { t.Run(fmt.Sprintf("%s", secrets), func(t *testing.T) { @@ -660,7 +710,7 @@ func TestReusableWorkflowMetadataFromASTNodeSecrets(t *testing.T) { want := ReusableWorkflowMetadataSecrets{} for n, r := range secrets { - want[n] = &ReusableWorkflowMetadataSecret{ + want[strings.ToLower(n)] = &ReusableWorkflowMetadataSecret{ Name: n, Required: r != nil && r.Value, } diff --git a/rule_workflow_call_test.go b/rule_workflow_call_test.go index 4f3022eee..5ece875ab 100644 --- a/rule_workflow_call_test.go +++ b/rule_workflow_call_test.go @@ -1,6 +1,7 @@ package actionlint import ( + "fmt" "path/filepath" "sort" "strings" @@ -153,22 +154,41 @@ func TestRuleWorkflowCallWriteEventNodeToMetadataCache(t *testing.T) { } func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { - metadata := &ReusableWorkflowMetadata{ - Inputs: ReusableWorkflowMetadataInputs{ - "optional_input": {"optional_input", false, StringType{}}, - "required_input": {"required_input", true, StringType{}}, - }, - Outputs: ReusableWorkflowMetadataOutputs{ - "output": {"output"}, + cwd := filepath.Join("testdata", "reusable_workflow_metadata") + cache := NewLocalReusableWorkflowCache(&Project{cwd, nil}, cwd, nil) + + for i, md := range []*ReusableWorkflowMetadata{ + // workflow0.yaml + { + Inputs: ReusableWorkflowMetadataInputs{ + "optional_input": {"optional_input", false, StringType{}}, + "required_input": {"required_input", true, StringType{}}, + }, + Outputs: ReusableWorkflowMetadataOutputs{ + "output": {"output"}, + }, + Secrets: ReusableWorkflowMetadataSecrets{ + "optional_secret": {"optional_secret", false}, + "required_secret": {"required_secret", true}, + }, }, - Secrets: ReusableWorkflowMetadataSecrets{ - "optional_secret": {"optional_secret", false}, - "required_secret": {"required_secret", true}, + // workflow1.yaml: Inputs and outputs in upper case (#216) + { + Inputs: ReusableWorkflowMetadataInputs{ + "optional_input": {"OPTIONAL_INPUT", false, StringType{}}, + "required_input": {"REQUIRED_INPUT", true, StringType{}}, + }, + Outputs: ReusableWorkflowMetadataOutputs{ + "output": {"OUTPUT"}, + }, + Secrets: ReusableWorkflowMetadataSecrets{ + "optional_secret": {"OPTIONAL_SECRET", false}, + "required_secret": {"REQUIRED_SECRET", true}, + }, }, + } { + cache.writeCache(fmt.Sprintf("./workflow%d.yaml", i), md) } - cwd := filepath.Join("testdata", "reusable_workflow_metadata") - cache := NewLocalReusableWorkflowCache(&Project{cwd, nil}, cwd, nil) - cache.writeCache("./workflow.yaml", metadata) tests := []struct { what string @@ -180,13 +200,13 @@ func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { }{ { what: "all", - uses: "./workflow.yaml", + uses: "./workflow0.yaml", inputs: []string{"optional_input", "required_input"}, secrets: []string{"optional_secret", "required_secret"}, }, { what: "only required", - uses: "./workflow.yaml", + uses: "./workflow0.yaml", inputs: []string{"required_input"}, secrets: []string{"required_secret"}, }, @@ -201,7 +221,7 @@ func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { }, { what: "missing required input and secret", - uses: "./workflow.yaml", + uses: "./workflow0.yaml", inputs: []string{"optional_input"}, secrets: []string{"optional_secret"}, errs: []string{ @@ -211,17 +231,17 @@ func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { }, { what: "undefined input and secret", - uses: "./workflow.yaml", + uses: "./workflow0.yaml", inputs: []string{"required_input", "unknown_input"}, secrets: []string{"required_secret", "unknown_secret"}, errs: []string{ - "input \"unknown_input\" is not defined in \"./workflow.yaml\" reusable workflow. defined inputs are \"optional_input\", \"required_input\"", - "secret \"unknown_secret\" is not defined in \"./workflow.yaml\" reusable workflow. defined secrets are \"optional_secret\", \"required_secret\"", + "input \"unknown_input\" is not defined in \"./workflow0.yaml\" reusable workflow. defined inputs are \"optional_input\", \"required_input\"", + "secret \"unknown_secret\" is not defined in \"./workflow0.yaml\" reusable workflow. defined secrets are \"optional_secret\", \"required_secret\"", }, }, { what: "inherit secrets", - uses: "./workflow.yaml", + uses: "./workflow0.yaml", inputs: []string{"required_input"}, secrets: []string{"unknown_secret", "optional_secret"}, inheritSecrets: true, @@ -249,6 +269,34 @@ func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { inputs: []string{"aaa", "bbb"}, secrets: []string{"xxx", "yyy"}, }, + { + what: "call in upper case and workflow in lower case", + uses: "./workflow0.yaml", + inputs: []string{"OPTIONAL_INPUT", "REQUIRED_INPUT"}, + secrets: []string{"OPTIONAL_SECRET", "REQUIRED_SECRET"}, + }, + { + what: "call in lower case and workflow in upper case", + uses: "./workflow1.yaml", + inputs: []string{"optional_input", "required_input"}, + secrets: []string{"optional_secret", "required_secret"}, + }, + { + what: "call in upper case and workflow in upper case", + uses: "./workflow1.yaml", + inputs: []string{"OPTIONAL_INPUT", "REQUIRED_INPUT"}, + secrets: []string{"OPTIONAL_SECRET", "REQUIRED_SECRET"}, + }, + { + what: "undefined upper input and secret", + uses: "./workflow0.yaml", + inputs: []string{"required_input", "UNKNOWN_INPUT"}, + secrets: []string{"required_secret", "UNKNOWN_SECRET"}, + errs: []string{ + "input \"UNKNOWN_INPUT\" is not defined in \"./workflow0.yaml\"", + "secret \"UNKNOWN_SECRET\" is not defined in \"./workflow0.yaml\"", + }, + }, } for _, tc := range tests { @@ -273,13 +321,13 @@ func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { InheritSecrets: tc.inheritSecrets, } for _, i := range tc.inputs { - c.Inputs[i] = &WorkflowCallInput{ + c.Inputs[strings.ToLower(i)] = &WorkflowCallInput{ Name: &String{Value: i, Pos: &Pos{}}, Value: &String{Value: "", Pos: &Pos{}}, } } for _, s := range tc.secrets { - c.Secrets[s] = &WorkflowCallSecret{ + c.Secrets[strings.ToLower(s)] = &WorkflowCallSecret{ Name: &String{Value: s, Pos: &Pos{}}, Value: &String{Value: "", Pos: &Pos{}}, } From c15d91178c87c310e7af97a5b417d7b19b40d400 Mon Sep 17 00:00:00 2001 From: rhysd Date: Thu, 22 Sep 2022 10:57:09 +0900 Subject: [PATCH 3/5] add tests for checking inputs/secrets of reusable workflow calls in case-insensitive in project level --- rule_workflow_call.go | 24 +++++++++---------- .../projects/workflow_call_upper_case.out | 6 +++++ .../reusable/lower.yaml | 18 ++++++++++++++ .../reusable/upper.yaml | 18 ++++++++++++++ .../workflows/missing.yaml | 9 +++++++ .../workflows/ok_lower.yaml | 11 +++++++++ .../workflows/ok_upper.yaml | 19 +++++++++++++++ .../workflows/undefined.yaml | 23 ++++++++++++++++++ 8 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 testdata/projects/workflow_call_upper_case.out create mode 100644 testdata/projects/workflow_call_upper_case/reusable/lower.yaml create mode 100644 testdata/projects/workflow_call_upper_case/reusable/upper.yaml create mode 100644 testdata/projects/workflow_call_upper_case/workflows/missing.yaml create mode 100644 testdata/projects/workflow_call_upper_case/workflows/ok_lower.yaml create mode 100644 testdata/projects/workflow_call_upper_case/workflows/ok_upper.yaml create mode 100644 testdata/projects/workflow_call_upper_case/workflows/undefined.yaml diff --git a/rule_workflow_call.go b/rule_workflow_call.go index 45c49c8b1..529c35fc0 100644 --- a/rule_workflow_call.go +++ b/rule_workflow_call.go @@ -101,14 +101,14 @@ func (rule *RuleWorkflowCall) checkWorkflowCallUsesLocal(call *WorkflowCall) { if _, ok := m.Inputs[n]; !ok { note := "no input is defined" if len(m.Inputs) > 0 { - i := make([]string, 0, len(m.Inputs)) - for n := range m.Inputs { - i = append(i, n) + is := make([]string, 0, len(m.Inputs)) + for _, i := range m.Inputs { + is = append(is, i.Name) } - if len(i) == 1 { - note = fmt.Sprintf("defined input is %q", i[0]) + if len(is) == 1 { + note = fmt.Sprintf("defined input is %q", is[0]) } else { - note = "defined inputs are " + sortedQuotes(i) + note = "defined inputs are " + sortedQuotes(is) } } rule.errorf(i.Name.Pos, "input %q is not defined in %q reusable workflow. %s", i.Name.Value, u.Value, note) @@ -128,14 +128,14 @@ func (rule *RuleWorkflowCall) checkWorkflowCallUsesLocal(call *WorkflowCall) { if _, ok := m.Secrets[n]; !ok { note := "no secret is defined" if len(m.Secrets) > 0 { - s := make([]string, 0, len(m.Secrets)) - for n := range m.Secrets { - s = append(s, n) + ss := make([]string, 0, len(m.Secrets)) + for _, s := range m.Secrets { + ss = append(ss, s.Name) } - if len(s) == 1 { - note = fmt.Sprintf("defined secret is %q", s[0]) + if len(ss) == 1 { + note = fmt.Sprintf("defined secret is %q", ss[0]) } else { - note = "defined secrets are " + sortedQuotes(s) + note = "defined secrets are " + sortedQuotes(ss) } } rule.errorf(s.Name.Pos, "secret %q is not defined in %q reusable workflow. %s", s.Name.Value, u.Value, note) diff --git a/testdata/projects/workflow_call_upper_case.out b/testdata/projects/workflow_call_upper_case.out new file mode 100644 index 000000000..69ed7b52a --- /dev/null +++ b/testdata/projects/workflow_call_upper_case.out @@ -0,0 +1,6 @@ +workflows/missing.yaml:5:11: input "MY_INPUT_2" is required by "./reusable/upper.yaml" reusable workflow [workflow-call] +workflows/missing.yaml:5:11: secret "MY_SECRET_2" is required by "./reusable/upper.yaml" reusable workflow [workflow-call] +workflows/undefined.yaml:9:7: input "my_input_3" is not defined in "./reusable/upper.yaml" reusable workflow. defined inputs are "MY_INPUT_1", "MY_INPUT_2" [workflow-call] +workflows/undefined.yaml:13:7: secret "my_secret_3" is not defined in "./reusable/upper.yaml" reusable workflow. defined secrets are "MY_SECRET_1", "MY_SECRET_2" [workflow-call] +workflows/undefined.yaml:19:7: input "my_input_3" is not defined in "./reusable/lower.yaml" reusable workflow. defined inputs are "my_input_1", "my_input_2" [workflow-call] +workflows/undefined.yaml:23:7: secret "my_secret_3" is not defined in "./reusable/lower.yaml" reusable workflow. defined secrets are "my_secret_1", "my_secret_2" [workflow-call] diff --git a/testdata/projects/workflow_call_upper_case/reusable/lower.yaml b/testdata/projects/workflow_call_upper_case/reusable/lower.yaml new file mode 100644 index 000000000..d6e8108a9 --- /dev/null +++ b/testdata/projects/workflow_call_upper_case/reusable/lower.yaml @@ -0,0 +1,18 @@ +on: + workflow_call: + inputs: + my_input_1: + required: true + my_input_2: + required: true + secrets: + my_secret_1: + required: true + my_secret_2: + required: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo '${{ inputs.my_input_1 }}' diff --git a/testdata/projects/workflow_call_upper_case/reusable/upper.yaml b/testdata/projects/workflow_call_upper_case/reusable/upper.yaml new file mode 100644 index 000000000..45d85f4e4 --- /dev/null +++ b/testdata/projects/workflow_call_upper_case/reusable/upper.yaml @@ -0,0 +1,18 @@ +on: + workflow_call: + inputs: + MY_INPUT_1: + required: true + MY_INPUT_2: + required: true + secrets: + MY_SECRET_1: + required: true + MY_SECRET_2: + required: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo '${{ inputs.my_input_1 }}' diff --git a/testdata/projects/workflow_call_upper_case/workflows/missing.yaml b/testdata/projects/workflow_call_upper_case/workflows/missing.yaml new file mode 100644 index 000000000..29db7c3c0 --- /dev/null +++ b/testdata/projects/workflow_call_upper_case/workflows/missing.yaml @@ -0,0 +1,9 @@ +on: push + +jobs: + upper: + uses: ./reusable/upper.yaml + with: + my_input_1: hello + secrets: + my_secret_1: hello diff --git a/testdata/projects/workflow_call_upper_case/workflows/ok_lower.yaml b/testdata/projects/workflow_call_upper_case/workflows/ok_lower.yaml new file mode 100644 index 000000000..b00050ce7 --- /dev/null +++ b/testdata/projects/workflow_call_upper_case/workflows/ok_lower.yaml @@ -0,0 +1,11 @@ +on: push + +jobs: + upper: + uses: ./reusable/upper.yaml + with: + my_input_1: hello + my_input_2: world + secrets: + my_secret_1: hello + my_secret_2: world diff --git a/testdata/projects/workflow_call_upper_case/workflows/ok_upper.yaml b/testdata/projects/workflow_call_upper_case/workflows/ok_upper.yaml new file mode 100644 index 000000000..e32b9ecd9 --- /dev/null +++ b/testdata/projects/workflow_call_upper_case/workflows/ok_upper.yaml @@ -0,0 +1,19 @@ +on: push + +jobs: + upper: + uses: ./reusable/upper.yaml + with: + MY_INPUT_1: hello + MY_INPUT_2: world + secrets: + MY_SECRET_1: hello + MY_SECRET_2: world + lower: + uses: ./reusable/lower.yaml + with: + MY_INPUT_1: hello + MY_INPUT_2: world + secrets: + MY_SECRET_1: hello + MY_SECRET_2: world diff --git a/testdata/projects/workflow_call_upper_case/workflows/undefined.yaml b/testdata/projects/workflow_call_upper_case/workflows/undefined.yaml new file mode 100644 index 000000000..64370f303 --- /dev/null +++ b/testdata/projects/workflow_call_upper_case/workflows/undefined.yaml @@ -0,0 +1,23 @@ +on: push + +jobs: + upper: + uses: ./reusable/upper.yaml + with: + MY_INPUT_1: hello + MY_INPUT_2: world + MY_INPUT_3: undefined + secrets: + MY_SECRET_1: hello + MY_SECRET_2: world + MY_SECRET_3: undefined + lower: + uses: ./reusable/lower.yaml + with: + MY_INPUT_1: hello + MY_INPUT_2: world + MY_INPUT_3: undefined + secrets: + MY_SECRET_1: hello + MY_SECRET_2: world + MY_SECRET_3: undefined From 263c68c4dddfd686f2b0764a331d69c58affb433 Mon Sep 17 00:00:00 2001 From: rhysd Date: Thu, 22 Sep 2022 11:53:21 +0900 Subject: [PATCH 4/5] add tests for reusable workflow call where no input/secret is defined --- rule_workflow_call_test.go | 16 ++++++++++++++++ testdata/projects/workflow_call_undefined.out | 2 ++ .../workflows/empty_reusable.yaml | 7 +++++++ .../workflows/reusable.yaml | 2 +- .../workflow_call_undefined/workflows/test.yaml | 6 ++++++ 5 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 testdata/projects/workflow_call_undefined/workflows/empty_reusable.yaml diff --git a/rule_workflow_call_test.go b/rule_workflow_call_test.go index 5ece875ab..2d8d131b4 100644 --- a/rule_workflow_call_test.go +++ b/rule_workflow_call_test.go @@ -186,6 +186,12 @@ func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { "required_secret": {"REQUIRED_SECRET", true}, }, }, + // workflow2.yaml: No input and secret are defined + { + Inputs: ReusableWorkflowMetadataInputs{}, + Outputs: ReusableWorkflowMetadataOutputs{}, + Secrets: ReusableWorkflowMetadataSecrets{}, + }, } { cache.writeCache(fmt.Sprintf("./workflow%d.yaml", i), md) } @@ -297,6 +303,16 @@ func TestRuleWorkflowCallCheckReusableWorkflowCall(t *testing.T) { "secret \"UNKNOWN_SECRET\" is not defined in \"./workflow0.yaml\"", }, }, + { + what: "no input and secret defined", + uses: "./workflow2.yaml", + inputs: []string{"unknown_input"}, + secrets: []string{"unknown_secret"}, + errs: []string{ + "input \"unknown_input\" is not defined in \"./workflow2.yaml\" reusable workflow. no input is defined", + "secret \"unknown_secret\" is not defined in \"./workflow2.yaml\" reusable workflow. no secret is defined", + }, + }, } for _, tc := range tests { diff --git a/testdata/projects/workflow_call_undefined.out b/testdata/projects/workflow_call_undefined.out index e7ecdb09f..bd0637a99 100644 --- a/testdata/projects/workflow_call_undefined.out +++ b/testdata/projects/workflow_call_undefined.out @@ -1,3 +1,5 @@ workflows/test.yaml:7:7: input "aaa" is not defined in "./workflows/reusable.yaml" reusable workflow. defined input is "foo" [workflow-call] workflows/test.yaml:10:7: secret "bbb" is not defined in "./workflows/reusable.yaml" reusable workflow. defined secret is "piyo" [workflow-call] workflows/test.yaml:17:24: property "ccc" is not defined in object type {bar: string} [expression] +workflows/test.yaml:21:7: input "input1" is not defined in "./workflows/empty_reusable.yaml" reusable workflow. no input is defined [workflow-call] +workflows/test.yaml:23:7: secret "secret1" is not defined in "./workflows/empty_reusable.yaml" reusable workflow. no secret is defined [workflow-call] diff --git a/testdata/projects/workflow_call_undefined/workflows/empty_reusable.yaml b/testdata/projects/workflow_call_undefined/workflows/empty_reusable.yaml new file mode 100644 index 000000000..6b94cb574 --- /dev/null +++ b/testdata/projects/workflow_call_undefined/workflows/empty_reusable.yaml @@ -0,0 +1,7 @@ +on: workflow_call + +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo 'hi' diff --git a/testdata/projects/workflow_call_undefined/workflows/reusable.yaml b/testdata/projects/workflow_call_undefined/workflows/reusable.yaml index 3d665230a..5007cfce5 100644 --- a/testdata/projects/workflow_call_undefined/workflows/reusable.yaml +++ b/testdata/projects/workflow_call_undefined/workflows/reusable.yaml @@ -13,4 +13,4 @@ jobs: test: runs-on: ubuntu-latest steps: - - run: 'bye' + - run: echo 'bye' diff --git a/testdata/projects/workflow_call_undefined/workflows/test.yaml b/testdata/projects/workflow_call_undefined/workflows/test.yaml index 4fb3d50e0..f385c99d9 100644 --- a/testdata/projects/workflow_call_undefined/workflows/test.yaml +++ b/testdata/projects/workflow_call_undefined/workflows/test.yaml @@ -15,3 +15,9 @@ jobs: steps: - run: echo '${{ needs.caller.outputs.bar }} is existing' - run: echo '${{ needs.caller.outputs.ccc }} is not existing' + empty: + uses: ./workflows/empty_reusable.yaml + with: + input1: this is not existing + secrets: + secret1: this is not existing From 90103e0c8298d2a94ebaa05df657dfc7dc95035f Mon Sep 17 00:00:00 2001 From: rhysd Date: Thu, 22 Sep 2022 12:01:47 +0900 Subject: [PATCH 5/5] test type check of outputs from workflow call in case-insensitive --- .../projects/workflow_call_upper_case.out | 4 +++ .../reusable/lower.yaml | 5 +++ .../reusable/upper.yaml | 5 +++ .../workflows/output.yaml | 35 +++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 testdata/projects/workflow_call_upper_case/workflows/output.yaml diff --git a/testdata/projects/workflow_call_upper_case.out b/testdata/projects/workflow_call_upper_case.out index 69ed7b52a..cea6389fe 100644 --- a/testdata/projects/workflow_call_upper_case.out +++ b/testdata/projects/workflow_call_upper_case.out @@ -1,5 +1,9 @@ workflows/missing.yaml:5:11: input "MY_INPUT_2" is required by "./reusable/upper.yaml" reusable workflow [workflow-call] workflows/missing.yaml:5:11: secret "MY_SECRET_2" is required by "./reusable/upper.yaml" reusable workflow [workflow-call] +workflows/output.yaml:32:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] +workflows/output.yaml:33:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] +workflows/output.yaml:34:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] +workflows/output.yaml:35:30: property "my_output_3" is not defined in object type {my_output_1: string; my_output_2: string} [expression] workflows/undefined.yaml:9:7: input "my_input_3" is not defined in "./reusable/upper.yaml" reusable workflow. defined inputs are "MY_INPUT_1", "MY_INPUT_2" [workflow-call] workflows/undefined.yaml:13:7: secret "my_secret_3" is not defined in "./reusable/upper.yaml" reusable workflow. defined secrets are "MY_SECRET_1", "MY_SECRET_2" [workflow-call] workflows/undefined.yaml:19:7: input "my_input_3" is not defined in "./reusable/lower.yaml" reusable workflow. defined inputs are "my_input_1", "my_input_2" [workflow-call] diff --git a/testdata/projects/workflow_call_upper_case/reusable/lower.yaml b/testdata/projects/workflow_call_upper_case/reusable/lower.yaml index d6e8108a9..77200af84 100644 --- a/testdata/projects/workflow_call_upper_case/reusable/lower.yaml +++ b/testdata/projects/workflow_call_upper_case/reusable/lower.yaml @@ -5,6 +5,11 @@ on: required: true my_input_2: required: true + outputs: + my_output_1: + value: ... + my_output_2: + value: ... secrets: my_secret_1: required: true diff --git a/testdata/projects/workflow_call_upper_case/reusable/upper.yaml b/testdata/projects/workflow_call_upper_case/reusable/upper.yaml index 45d85f4e4..21a782a20 100644 --- a/testdata/projects/workflow_call_upper_case/reusable/upper.yaml +++ b/testdata/projects/workflow_call_upper_case/reusable/upper.yaml @@ -5,6 +5,11 @@ on: required: true MY_INPUT_2: required: true + outputs: + MY_OUTPUT_1: + value: ... + MY_OUTPUT_2: + value: ... secrets: MY_SECRET_1: required: true diff --git a/testdata/projects/workflow_call_upper_case/workflows/output.yaml b/testdata/projects/workflow_call_upper_case/workflows/output.yaml new file mode 100644 index 000000000..3e27e21ad --- /dev/null +++ b/testdata/projects/workflow_call_upper_case/workflows/output.yaml @@ -0,0 +1,35 @@ +on: push + +jobs: + upper: + uses: ./reusable/upper.yaml + with: + my_input_1: hello + my_input_2: world + secrets: + my_secret_1: hello + my_secret_2: world + lower: + uses: ./reusable/lower.yaml + with: + my_input_1: hello + my_input_2: world + secrets: + my_secret_1: hello + my_secret_2: world + downstream: + needs: [upper, lower] + runs-on: ubuntu-latest + steps: + - run: echo 'OK ${{ needs.upper.outputs.my_output_1 }}' + - run: echo 'OK ${{ needs.upper.outputs.MY_OUTPUT_1 }}' + - run: echo 'OK ${{ needs.lower.outputs.my_output_1 }}' + - run: echo 'OK ${{ needs.lower.outputs.MY_OUTPUT_1 }}' + - run: echo 'OK ${{ needs.upper.outputs.my_output_2 }}' + - run: echo 'OK ${{ needs.upper.outputs.MY_OUTPUT_2 }}' + - run: echo 'OK ${{ needs.lower.outputs.my_output_2 }}' + - run: echo 'OK ${{ needs.lower.outputs.MY_OUTPUT_2 }}' + - run: echo 'ERROR ${{ needs.upper.outputs.my_output_3 }}' + - run: echo 'ERROR ${{ needs.upper.outputs.MY_OUTPUT_3 }}' + - run: echo 'ERROR ${{ needs.lower.outputs.my_output_3 }}' + - run: echo 'ERROR ${{ needs.lower.outputs.MY_OUTPUT_3 }}'