We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Support for multiple CodePipeline OutputArtifacts
*cfn-lint version*: 0.12.1

*Description of issue.*

The CloudFormation linter does not yet support having multiple values for the OutputArtifacts property. When linting a template it gives the following error message:
`E2541 Action "CodeBuild" declares 2 OutputArtifacts which is not in expected range [0, 1].`

```yaml
---
AWSTemplateFormatVersion: 2010-09-09
Resources:
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: pipeline
      RoleArn: 'rolearn'
      RestartExecutionOnUpdate: true
      ArtifactStore:
        Location: 'artifactbucket'
        Type: S3
      Stages:
        - Name: Source
          Actions:
            - Name: SourceRepo
              ActionTypeId:
                # More info on Possible Values: https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements
                Category: Source
                Owner: ThirdParty
                Provider: GitHub
                Version: "1"
              Configuration:
                Owner: '{{resolve:ssm:/service/github/owner:1}}'
                OAuthToken: '{{resolve:ssm:/service/github/token:3}}'
                Repo: 'repo'
                Branch: 'develop'
                PollForSourceChanges: true
              OutputArtifacts:
                - Name: source
              RunOrder: 1
        - Name: Build
          Actions:
            - Name: CodeBuild
              ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: "1"
              Configuration:
                ProjectName: 'codebuildproject'
              InputArtifacts:
                - Name: source
              OutputArtifacts:
                - Name: artifact1
                - Name: artifact2 # this goes wrong
              RunOrder: 1
```

As additional information a [blog post](https://aws.amazon.com/about-aws/whats-new/2018/08/aws-codebuild-adds-ability-to-create-build-projects-with-multiple-input-sources-and-output-artifacts/) about the release of support for this and the [CloudFormation spec](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html).

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py`
Content:
```
1 """
2   Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 
4   Permission is hereby granted, free of charge, to any person obtaining a copy of this
5   software and associated documentation files (the "Software"), to deal in the Software
6   without restriction, including without limitation the rights to use, copy, modify,
7   merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
8   permit persons to whom the Software is furnished to do so.
9 
10   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
11   INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
12   PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
13   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
14   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 """
17 from cfnlint import CloudFormationLintRule
18 from cfnlint import RuleMatch
19 
20 
21 class CodepipelineStageActions(CloudFormationLintRule):
22     """Check if CodePipeline Stage Actions are set up properly."""
23     id = 'E2541'
24     shortdesc = 'CodePipeline Stage Actions'
25     description = 'See if CodePipeline stage actions are set correctly'
26     source_url = 'https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements'
27     tags = ['resources', 'codepipeline']
28 
29     VALID_OWNER_STRINGS = {'AWS', 'ThirdParty', 'Custom'}
30 
31     CONSTRAINTS = {
32         'AWS': {
33             'Source': {
34                 'S3': {
35                     'InputArtifactRange': 0,
36                     'OutputArtifactRange': 1,
37                 },
38                 'CodeCommit': {
39                     'InputArtifactRange': 0,
40                     'OutputArtifactRange': 1,
41                 }
42             },
43             'Build': {
44                 'CodeBuild': {
45                     'InputArtifactRange': 1,
46                     'OutputArtifactRange': (0, 1),
47                 },
48             },
49             'Test': {
50                 'CodeBuild': {
51                     'InputArtifactRange': 1,
52                     'OutputArtifactRange': (0, 1),
53                 }
54             },
55             'Approval': {
56                 'Manual': {
57                     'InputArtifactRange': 0,
58                     'OutputArtifactRange': 0,
59                 }
60             },
61             'Deploy': {
62                 'CloudFormation': {
63                     'InputArtifactRange': (0, 10),
64                     'OutputArtifactRange': (0, 1),
65                 },
66                 'CodeDeploy': {
67                     'InputArtifactRange': 1,
68                     'OutputArtifactRange': 0,
69                 },
70                 'ElasticBeanstalk': {
71                     'InputArtifactRange': 1,
72                     'OutputArtifactRange': 0,
73                 },
74                 'OpsWorks': {
75                     'InputArtifactRange': 1,
76                     'OutputArtifactRange': 0,
77                 },
78                 'ECS': {
79                     'InputArtifactRange': 1,
80                     'OutputArtifactRange': 0,
81                 },
82             },
83             'Invoke': {
84                 'Lambda': {
85                     'InputArtifactRange': (0, 5),
86                     'OutputArtifactRange': (0, 5),
87                 }
88             }
89         },
90         'ThirdParty': {
91             'Source': {
92                 'GitHub': {
93                     'InputArtifactRange': 0,
94                     'OutputArtifactRange': 1,
95                 }
96             },
97         },
98     }
99 
100     KEY_MAP = {
101         'InputArtifacts': 'InputArtifactRange',
102         'OutputArtifacts': 'OutputArtifactRange',
103     }
104 
105     def check_artifact_counts(self, action, artifact_type, path):
106         """Check that artifact counts are within valid ranges."""
107         matches = []
108 
109         action_type_id = action.get('ActionTypeId')
110         owner = action_type_id.get('Owner')
111         category = action_type_id.get('Category')
112         provider = action_type_id.get('Provider')
113 
114         if isinstance(owner, dict) or isinstance(category, dict) or isinstance(provider, dict):
115             self.logger.debug('owner, category, provider need to be strings to validate. Skipping.')
116             return matches
117 
118         constraints = self.CONSTRAINTS.get(owner, {}).get(category, {}).get(provider, {})
119         if not constraints:
120             return matches
121         artifact_count = len(action.get(artifact_type, []))
122 
123         constraint_key = self.KEY_MAP[artifact_type]
124         if isinstance(constraints[constraint_key], tuple):
125             min_, max_ = constraints[constraint_key]
126             if not (min_ <= artifact_count <= max_):
127                 message = (
128                     'Action "{action}" declares {number} {artifact_type} which is not in '
129                     'expected range [{a}, {b}].'
130                 ).format(
131                     action=action['Name'],
132                     number=artifact_count,
133                     artifact_type=artifact_type,
134                     a=min_,
135                     b=max_
136                 )
137                 matches.append(RuleMatch(
138                     path + [artifact_type],
139                     message
140                 ))
141         else:
142             if artifact_count != constraints[constraint_key]:
143                 message = (
144                     'Action "{action}" declares {number} {artifact_type} which is not the '
145                     'expected number [{a}].'
146                 ).format(
147                     action=action['Name'],
148                     number=artifact_count,
149                     artifact_type=artifact_type,
150                     a=constraints[constraint_key]
151                 )
152                 matches.append(RuleMatch(
153                     path + [artifact_type],
154                     message
155                 ))
156 
157         return matches
158 
159     def check_owner(self, action, path):
160         """Check that action type owner is valid."""
161         matches = []
162 
163         owner = action.get('ActionTypeId').get('Owner')
164         if owner not in self.VALID_OWNER_STRINGS and owner is not None:
165             message = (
166                 'For all currently supported action types, the only valid owner '
167                 'strings are {owners}'
168             ).format(
169                 owners=', '.join(list(self.VALID_OWNER_STRINGS))
170             )
171             matches.append(RuleMatch(
172                 path + ['ActionTypeId', 'Owner'],
173                 message
174             ))
175 
176         return matches
177 
178     def check_version(self, action, path):
179         """Check that action type version is valid."""
180         matches = []
181 
182         version = action.get('ActionTypeId', {}).get('Version')
183         if isinstance(version, dict):
184             self.logger.debug('Unable to validate version when an object is used.  Skipping')
185         elif version != '1':
186             message = 'For all currently supported action types, the only valid version string is "1".'
187             matches.append(RuleMatch(
188                 path + ['ActionTypeId', 'Version'],
189                 message
190             ))
191         return matches
192 
193     def check_names_unique(self, action, path, action_names):
194         """Check that action names are unique."""
195         matches = []
196 
197         if action.get('Name') in action_names:
198             message = 'All action names within a stage must be unique. ({name})'.format(
199                 name=action.get('Name')
200             )
201             matches.append(RuleMatch(path + ['Name'], message))
202         action_names.add(action.get('Name'))
203 
204         return matches
205 
206     def match(self, cfn):
207         """Check that stage actions are set up properly."""
208         matches = []
209 
210         resources = cfn.get_resource_properties(['AWS::CodePipeline::Pipeline'])
211         for resource in resources:
212             path = resource['Path']
213             properties = resource['Value']
214 
215             s_stages = properties.get_safe('Stages', path)
216             for s_stage_v, s_stage_p in s_stages:
217                 if not isinstance(s_stage_v, list):
218                     self.logger.debug('Stages not list. Should have been caught by generic linting.')
219                     return matches
220 
221                 for l_i_stage, l_i_path in s_stage_v.items_safe(s_stage_p):
222                     action_names = set()
223                     s_actions = l_i_stage.get_safe('Actions', l_i_path)
224                     for s_action_v, s_action_p in s_actions:
225                         if not isinstance(s_action_v, list):
226                             self.logger.debug('Actions not list. Should have been caught by generic linting.')
227                             return matches
228 
229                         for l_i_a_action, l_i_a_path in s_action_v.items_safe(s_action_p):
230                             try:
231                                 matches.extend(self.check_names_unique(l_i_a_action, l_i_a_path, action_names))
232                                 matches.extend(self.check_version(l_i_a_action, l_i_a_path))
233                                 matches.extend(self.check_owner(l_i_a_action, l_i_a_path))
234                                 matches.extend(self.check_artifact_counts(l_i_a_action, 'InputArtifacts', l_i_a_path))
235                                 matches.extend(self.check_artifact_counts(l_i_a_action, 'OutputArtifacts', l_i_a_path))
236                             except AttributeError as err:
237                                 self.logger.debug('Got AttributeError. Should have been caught by generic linting. '
238                                                   'Ignoring the error here: %s', str(err))
239 
240         return matches
241
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
--- a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
+++ b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py
@@ -40,12 +40,6 @@
                     'OutputArtifactRange': 1,
                 }
             },
-            'Build': {
-                'CodeBuild': {
-                    'InputArtifactRange': 1,
-                    'OutputArtifactRange': (0, 1),
-                },
-            },
             'Test': {
                 'CodeBuild': {
                     'InputArtifactRange': 1,

{"golden_diff": "diff --git a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py\n--- a/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py\n+++ b/src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py\n@@ -40,12 +40,6 @@\n                     'OutputArtifactRange': 1,\n                 }\n             },\n-            'Build': {\n-                'CodeBuild': {\n-                    'InputArtifactRange': 1,\n-                    'OutputArtifactRange': (0, 1),\n-                },\n-            },\n             'Test': {\n                 'CodeBuild': {\n                     'InputArtifactRange': 1,\n", "issue": "Support for multiple CodePipeline OutputArtifacts\n*cfn-lint version*: 0.12.1\r\n\r\n*Description of issue.*\r\n\r\nThe CloudFormation linter does not yet support having multiple values for the OutputArtifacts property. When linting a template it gives the following error message:\r\n`E2541 Action \"CodeBuild\" declares 2 OutputArtifacts which is not in expected range [0, 1].`\r\n\r\n```yaml\r\n---\r\nAWSTemplateFormatVersion: 2010-09-09\r\nResources:\r\n  Pipeline:\r\n    Type: AWS::CodePipeline::Pipeline\r\n    Properties:\r\n      Name: pipeline\r\n      RoleArn: 'rolearn'\r\n      RestartExecutionOnUpdate: true\r\n      ArtifactStore:\r\n        Location: 'artifactbucket'\r\n        Type: S3\r\n      Stages:\r\n        - Name: Source\r\n          Actions:\r\n            - Name: SourceRepo\r\n              ActionTypeId:\r\n                # More info on Possible Values: https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements\r\n                Category: Source\r\n                Owner: ThirdParty\r\n                Provider: GitHub\r\n                Version: \"1\"\r\n              Configuration:\r\n                Owner: '{{resolve:ssm:/service/github/owner:1}}'\r\n                OAuthToken: '{{resolve:ssm:/service/github/token:3}}'\r\n                Repo: 'repo'\r\n                Branch: 'develop'\r\n                PollForSourceChanges: true\r\n              OutputArtifacts:\r\n                - Name: source\r\n              RunOrder: 1\r\n        - Name: Build\r\n          Actions:\r\n            - Name: CodeBuild\r\n              ActionTypeId:\r\n                Category: Build\r\n                Owner: AWS\r\n                Provider: CodeBuild\r\n                Version: \"1\"\r\n              Configuration:\r\n                ProjectName: 'codebuildproject'\r\n              InputArtifacts:\r\n                - Name: source\r\n              OutputArtifacts:\r\n                - Name: artifact1\r\n                - Name: artifact2 # this goes wrong\r\n              RunOrder: 1\r\n```\r\n\r\nAs additional information a [blog post](https://aws.amazon.com/about-aws/whats-new/2018/08/aws-codebuild-adds-ability-to-create-build-projects-with-multiple-input-sources-and-output-artifacts/) about the release of support for this and the [CloudFormation spec](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codepipeline-pipeline-stages-actions.html).\n", "before_files": [{"content": "\"\"\"\n  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\n\n\nclass CodepipelineStageActions(CloudFormationLintRule):\n    \"\"\"Check if CodePipeline Stage Actions are set up properly.\"\"\"\n    id = 'E2541'\n    shortdesc = 'CodePipeline Stage Actions'\n    description = 'See if CodePipeline stage actions are set correctly'\n    source_url = 'https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements'\n    tags = ['resources', 'codepipeline']\n\n    VALID_OWNER_STRINGS = {'AWS', 'ThirdParty', 'Custom'}\n\n    CONSTRAINTS = {\n        'AWS': {\n            'Source': {\n                'S3': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'CodeCommit': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n            'Build': {\n                'CodeBuild': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': (0, 1),\n                },\n            },\n            'Test': {\n                'CodeBuild': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': (0, 1),\n                }\n            },\n            'Approval': {\n                'Manual': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Deploy': {\n                'CloudFormation': {\n                    'InputArtifactRange': (0, 10),\n                    'OutputArtifactRange': (0, 1),\n                },\n                'CodeDeploy': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ElasticBeanstalk': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'OpsWorks': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ECS': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n            },\n            'Invoke': {\n                'Lambda': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            }\n        },\n        'ThirdParty': {\n            'Source': {\n                'GitHub': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n        },\n    }\n\n    KEY_MAP = {\n        'InputArtifacts': 'InputArtifactRange',\n        'OutputArtifacts': 'OutputArtifactRange',\n    }\n\n    def check_artifact_counts(self, action, artifact_type, path):\n        \"\"\"Check that artifact counts are within valid ranges.\"\"\"\n        matches = []\n\n        action_type_id = action.get('ActionTypeId')\n        owner = action_type_id.get('Owner')\n        category = action_type_id.get('Category')\n        provider = action_type_id.get('Provider')\n\n        if isinstance(owner, dict) or isinstance(category, dict) or isinstance(provider, dict):\n            self.logger.debug('owner, category, provider need to be strings to validate. Skipping.')\n            return matches\n\n        constraints = self.CONSTRAINTS.get(owner, {}).get(category, {}).get(provider, {})\n        if not constraints:\n            return matches\n        artifact_count = len(action.get(artifact_type, []))\n\n        constraint_key = self.KEY_MAP[artifact_type]\n        if isinstance(constraints[constraint_key], tuple):\n            min_, max_ = constraints[constraint_key]\n            if not (min_ <= artifact_count <= max_):\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not in '\n                    'expected range [{a}, {b}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=min_,\n                    b=max_\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n        else:\n            if artifact_count != constraints[constraint_key]:\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not the '\n                    'expected number [{a}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=constraints[constraint_key]\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n\n        return matches\n\n    def check_owner(self, action, path):\n        \"\"\"Check that action type owner is valid.\"\"\"\n        matches = []\n\n        owner = action.get('ActionTypeId').get('Owner')\n        if owner not in self.VALID_OWNER_STRINGS and owner is not None:\n            message = (\n                'For all currently supported action types, the only valid owner '\n                'strings are {owners}'\n            ).format(\n                owners=', '.join(list(self.VALID_OWNER_STRINGS))\n            )\n            matches.append(RuleMatch(\n                path + ['ActionTypeId', 'Owner'],\n                message\n            ))\n\n        return matches\n\n    def check_version(self, action, path):\n        \"\"\"Check that action type version is valid.\"\"\"\n        matches = []\n\n        version = action.get('ActionTypeId', {}).get('Version')\n        if isinstance(version, dict):\n            self.logger.debug('Unable to validate version when an object is used.  Skipping')\n        elif version != '1':\n            message = 'For all currently supported action types, the only valid version string is \"1\".'\n            matches.append(RuleMatch(\n                path + ['ActionTypeId', 'Version'],\n                message\n            ))\n        return matches\n\n    def check_names_unique(self, action, path, action_names):\n        \"\"\"Check that action names are unique.\"\"\"\n        matches = []\n\n        if action.get('Name') in action_names:\n            message = 'All action names within a stage must be unique. ({name})'.format(\n                name=action.get('Name')\n            )\n            matches.append(RuleMatch(path + ['Name'], message))\n        action_names.add(action.get('Name'))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check that stage actions are set up properly.\"\"\"\n        matches = []\n\n        resources = cfn.get_resource_properties(['AWS::CodePipeline::Pipeline'])\n        for resource in resources:\n            path = resource['Path']\n            properties = resource['Value']\n\n            s_stages = properties.get_safe('Stages', path)\n            for s_stage_v, s_stage_p in s_stages:\n                if not isinstance(s_stage_v, list):\n                    self.logger.debug('Stages not list. Should have been caught by generic linting.')\n                    return matches\n\n                for l_i_stage, l_i_path in s_stage_v.items_safe(s_stage_p):\n                    action_names = set()\n                    s_actions = l_i_stage.get_safe('Actions', l_i_path)\n                    for s_action_v, s_action_p in s_actions:\n                        if not isinstance(s_action_v, list):\n                            self.logger.debug('Actions not list. Should have been caught by generic linting.')\n                            return matches\n\n                        for l_i_a_action, l_i_a_path in s_action_v.items_safe(s_action_p):\n                            try:\n                                matches.extend(self.check_names_unique(l_i_a_action, l_i_a_path, action_names))\n                                matches.extend(self.check_version(l_i_a_action, l_i_a_path))\n                                matches.extend(self.check_owner(l_i_a_action, l_i_a_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'InputArtifacts', l_i_a_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'OutputArtifacts', l_i_a_path))\n                            except AttributeError as err:\n                                self.logger.debug('Got AttributeError. Should have been caught by generic linting. '\n                                                  'Ignoring the error here: %s', str(err))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py"}], "after_files": [{"content": "\"\"\"\n  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\n  Permission is hereby granted, free of charge, to any person obtaining a copy of this\n  software and associated documentation files (the \"Software\"), to deal in the Software\n  without restriction, including without limitation the rights to use, copy, modify,\n  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n  permit persons to whom the Software is furnished to do so.\n\n  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\n  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\nfrom cfnlint import CloudFormationLintRule\nfrom cfnlint import RuleMatch\n\n\nclass CodepipelineStageActions(CloudFormationLintRule):\n    \"\"\"Check if CodePipeline Stage Actions are set up properly.\"\"\"\n    id = 'E2541'\n    shortdesc = 'CodePipeline Stage Actions'\n    description = 'See if CodePipeline stage actions are set correctly'\n    source_url = 'https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements'\n    tags = ['resources', 'codepipeline']\n\n    VALID_OWNER_STRINGS = {'AWS', 'ThirdParty', 'Custom'}\n\n    CONSTRAINTS = {\n        'AWS': {\n            'Source': {\n                'S3': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'CodeCommit': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n            'Test': {\n                'CodeBuild': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': (0, 1),\n                }\n            },\n            'Approval': {\n                'Manual': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Deploy': {\n                'CloudFormation': {\n                    'InputArtifactRange': (0, 10),\n                    'OutputArtifactRange': (0, 1),\n                },\n                'CodeDeploy': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ElasticBeanstalk': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'OpsWorks': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n                'ECS': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\n            },\n            'Invoke': {\n                'Lambda': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            }\n        },\n        'ThirdParty': {\n            'Source': {\n                'GitHub': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n        },\n    }\n\n    KEY_MAP = {\n        'InputArtifacts': 'InputArtifactRange',\n        'OutputArtifacts': 'OutputArtifactRange',\n    }\n\n    def check_artifact_counts(self, action, artifact_type, path):\n        \"\"\"Check that artifact counts are within valid ranges.\"\"\"\n        matches = []\n\n        action_type_id = action.get('ActionTypeId')\n        owner = action_type_id.get('Owner')\n        category = action_type_id.get('Category')\n        provider = action_type_id.get('Provider')\n\n        if isinstance(owner, dict) or isinstance(category, dict) or isinstance(provider, dict):\n            self.logger.debug('owner, category, provider need to be strings to validate. Skipping.')\n            return matches\n\n        constraints = self.CONSTRAINTS.get(owner, {}).get(category, {}).get(provider, {})\n        if not constraints:\n            return matches\n        artifact_count = len(action.get(artifact_type, []))\n\n        constraint_key = self.KEY_MAP[artifact_type]\n        if isinstance(constraints[constraint_key], tuple):\n            min_, max_ = constraints[constraint_key]\n            if not (min_ <= artifact_count <= max_):\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not in '\n                    'expected range [{a}, {b}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=min_,\n                    b=max_\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n        else:\n            if artifact_count != constraints[constraint_key]:\n                message = (\n                    'Action \"{action}\" declares {number} {artifact_type} which is not the '\n                    'expected number [{a}].'\n                ).format(\n                    action=action['Name'],\n                    number=artifact_count,\n                    artifact_type=artifact_type,\n                    a=constraints[constraint_key]\n                )\n                matches.append(RuleMatch(\n                    path + [artifact_type],\n                    message\n                ))\n\n        return matches\n\n    def check_owner(self, action, path):\n        \"\"\"Check that action type owner is valid.\"\"\"\n        matches = []\n\n        owner = action.get('ActionTypeId').get('Owner')\n        if owner not in self.VALID_OWNER_STRINGS and owner is not None:\n            message = (\n                'For all currently supported action types, the only valid owner '\n                'strings are {owners}'\n            ).format(\n                owners=', '.join(list(self.VALID_OWNER_STRINGS))\n            )\n            matches.append(RuleMatch(\n                path + ['ActionTypeId', 'Owner'],\n                message\n            ))\n\n        return matches\n\n    def check_version(self, action, path):\n        \"\"\"Check that action type version is valid.\"\"\"\n        matches = []\n\n        version = action.get('ActionTypeId', {}).get('Version')\n        if isinstance(version, dict):\n            self.logger.debug('Unable to validate version when an object is used.  Skipping')\n        elif version != '1':\n            message = 'For all currently supported action types, the only valid version string is \"1\".'\n            matches.append(RuleMatch(\n                path + ['ActionTypeId', 'Version'],\n                message\n            ))\n        return matches\n\n    def check_names_unique(self, action, path, action_names):\n        \"\"\"Check that action names are unique.\"\"\"\n        matches = []\n\n        if action.get('Name') in action_names:\n            message = 'All action names within a stage must be unique. ({name})'.format(\n                name=action.get('Name')\n            )\n            matches.append(RuleMatch(path + ['Name'], message))\n        action_names.add(action.get('Name'))\n\n        return matches\n\n    def match(self, cfn):\n        \"\"\"Check that stage actions are set up properly.\"\"\"\n        matches = []\n\n        resources = cfn.get_resource_properties(['AWS::CodePipeline::Pipeline'])\n        for resource in resources:\n            path = resource['Path']\n            properties = resource['Value']\n\n            s_stages = properties.get_safe('Stages', path)\n            for s_stage_v, s_stage_p in s_stages:\n                if not isinstance(s_stage_v, list):\n                    self.logger.debug('Stages not list. Should have been caught by generic linting.')\n                    return matches\n\n                for l_i_stage, l_i_path in s_stage_v.items_safe(s_stage_p):\n                    action_names = set()\n                    s_actions = l_i_stage.get_safe('Actions', l_i_path)\n                    for s_action_v, s_action_p in s_actions:\n                        if not isinstance(s_action_v, list):\n                            self.logger.debug('Actions not list. Should have been caught by generic linting.')\n                            return matches\n\n                        for l_i_a_action, l_i_a_path in s_action_v.items_safe(s_action_p):\n                            try:\n                                matches.extend(self.check_names_unique(l_i_a_action, l_i_a_path, action_names))\n                                matches.extend(self.check_version(l_i_a_action, l_i_a_path))\n                                matches.extend(self.check_owner(l_i_a_action, l_i_a_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'InputArtifacts', l_i_a_path))\n                                matches.extend(self.check_artifact_counts(l_i_a_action, 'OutputArtifacts', l_i_a_path))\n                            except AttributeError as err:\n                                self.logger.debug('Got AttributeError. Should have been caught by generic linting. '\n                                                  'Ignoring the error here: %s', str(err))\n\n        return matches\n", "path": "src/cfnlint/rules/resources/codepipeline/CodepipelineStageActions.py"}]}