We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Doesn't catch CodePipeline OutputArtifacts need to be uniquely named
cfn-lint 0.35.1

*Description of issue.*
The linter doesn't catch that CodePipeline `OutputArtifacts` need to be uniquely named.

Please provide as much information as possible:
* Template linting issues: 
  * Please provide a CloudFormation sample that generated the issue.

This template generates the error `UPDATE_FAILED | Output Artifact Bundle name must be unique within the pipeline. CreateOutput has been used more than once.`

<details>

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: The AWS CloudFormation template for this Serverless application
Resources:
  ServerlessDeploymentPipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStores:
        - Region: ca-central-1
          ArtifactStore:
            Type: S3
            Location: my-artifact-bucket
      Name: my-code-pipeline
      RestartExecutionOnUpdate: false
      RoleArn: arn:aws:iam::000000000000:role/root
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: "1"
                Provider: S3
              OutputArtifacts:
                - Name: SourceArtifact
              Configuration:
                S3Bucket: my-source-bucket
                S3ObjectKey: source-item.zip
              RunOrder: 1
        - Name: DeployToEnvA
          Actions:
            - Name: CreateChangeSetEnvA
              Region: us-east-1
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: "1"
                Provider: CloudFormation
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: CreateOutput
              Configuration:
                ActionMode: CHANGE_SET_REPLACE
                StackName: my-service-env-a
                Capabilities: CAPABILITY_NAMED_IAM
                RoleArn: arn:aws:iam::000000000000:role/root
                TemplatePath: SourceArtifact::env-a-us-east-1.json
                ChangeSetName: ChangeSet
              RunOrder: 1
              RoleArn: arn:aws:iam::000000000000:role/root
            - Name: CreateChangeSetEnvB
              Region: us-east-1
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: "1"
                Provider: CloudFormation
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: CreateOutput
              Configuration:
                ActionMode: CHANGE_SET_REPLACE
                StackName: my-service-env-b
                Capabilities: CAPABILITY_NAMED_IAM
                RoleArn: arn:aws:iam::000000000000:role/root
                TemplatePath: SourceArtifact::env-b-us-east-1.json
                ChangeSetName: ChangeSet
              RunOrder: 1
              RoleArn: arn:aws:iam::000000000000:role/root

```

</details>

  * If present, please add links to the (official) documentation for clarification.
      - > Every output artifact in the pipeline must have a unique name.  

          [Source](https://docs.aws.amazon.com/codepipeline/latest/userguide/welcome-introducing-artifacts.html)
  * Validate if the issue still exists with the latest version of `cfn-lint` and/or the latest Spec files: :heavy_check_mark:  `0.35.1` is the latest version



Cfn-lint uses the [CloudFormation Resource Specifications](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) as the base to do validation. These files are included as part of the application version. Please update to the latest version of `cfn-lint` or update the spec files manually (`cfn-lint -u`)

:heavy_check_mark:  I have also tried after running `cfn-lint -u`

--- 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 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 SPDX-License-Identifier: MIT-0
4 """
5 import re
6 import six
7 from cfnlint.rules import CloudFormationLintRule
8 from cfnlint.rules import RuleMatch
9 
10 
11 class CodepipelineStageActions(CloudFormationLintRule):
12     """Check if CodePipeline Stage Actions are set up properly."""
13     id = 'E2541'
14     shortdesc = 'CodePipeline Stage Actions'
15     description = 'See if CodePipeline stage actions are set correctly'
16     source_url = 'https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#pipeline-requirements'
17     tags = ['resources', 'codepipeline']
18 
19     CONSTRAINTS = {
20         'AWS': {
21             'Source': {
22                 'S3': {
23                     'InputArtifactRange': 0,
24                     'OutputArtifactRange': 1,
25                 },
26                 'CodeCommit': {
27                     'InputArtifactRange': 0,
28                     'OutputArtifactRange': 1,
29                 },
30                 'ECR': {
31                     'InputArtifactRange': 0,
32                     'OutputArtifactRange': 1,
33                 }
34             },
35             'Test': {
36                 'CodeBuild': {
37                     'InputArtifactRange': (1, 5),
38                     'OutputArtifactRange': (0, 5),
39                 },
40                 'DeviceFarm': {
41                     'InputArtifactRange': 1,
42                     'OutputArtifactRange': 0,
43                 }
44             },
45             'Build': {
46                 'CodeBuild': {
47                     'InputArtifactRange': (1, 5),
48                     'OutputArtifactRange': (0, 5),
49                 }
50             },
51             'Approval': {
52                 'Manual': {
53                     'InputArtifactRange': 0,
54                     'OutputArtifactRange': 0,
55                 }
56             },
57             'Deploy': {
58                 'S3': {
59                     'InputArtifactRange': 1,
60                     'OutputArtifactRange': 0,
61                 },
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                 'ServiceCatalog': {
83                     'InputArtifactRange': 1,
84                     'OutputArtifactRange': 0,
85                 },
86             },
87             'Invoke': {
88                 'Lambda': {
89                     'InputArtifactRange': (0, 5),
90                     'OutputArtifactRange': (0, 5),
91                 }
92             }
93         },
94         'ThirdParty': {
95             'Source': {
96                 'GitHub': {
97                     'InputArtifactRange': 0,
98                     'OutputArtifactRange': 1,
99                 }
100             },
101             'Deploy': {
102                 'AlexaSkillsKit': {
103                     'InputArtifactRange': (0, 2),
104                     'OutputArtifactRange': 0,
105                 },
106             },
107         },
108         'Custom': {
109             'Build': {
110                 'Jenkins': {
111                     'InputArtifactRange': (0, 5),
112                     'OutputArtifactRange': (0, 5),
113                 },
114             },
115             'Test': {
116                 'Jenkins': {
117                     'InputArtifactRange': (0, 5),
118                     'OutputArtifactRange': (0, 5),
119                 },
120             },
121         },
122     }
123 
124     KEY_MAP = {
125         'InputArtifacts': 'InputArtifactRange',
126         'OutputArtifacts': 'OutputArtifactRange',
127     }
128 
129     def check_artifact_counts(self, action, artifact_type, path):
130         """Check that artifact counts are within valid ranges."""
131         matches = []
132 
133         action_type_id = action.get('ActionTypeId')
134         owner = action_type_id.get('Owner')
135         category = action_type_id.get('Category')
136         provider = action_type_id.get('Provider')
137 
138         if isinstance(owner, dict) or isinstance(category, dict) or isinstance(provider, dict):
139             self.logger.debug('owner, category, provider need to be strings to validate. Skipping.')
140             return matches
141 
142         constraints = self.CONSTRAINTS.get(owner, {}).get(category, {}).get(provider, {})
143         if not constraints:
144             return matches
145         artifact_count = len(action.get(artifact_type, []))
146 
147         constraint_key = self.KEY_MAP[artifact_type]
148         if isinstance(constraints[constraint_key], tuple):
149             min_, max_ = constraints[constraint_key]
150             if not (min_ <= artifact_count <= max_):
151                 message = (
152                     'Action "{action}" declares {number} {artifact_type} which is not in '
153                     'expected range [{a}, {b}].'
154                 ).format(
155                     action=action['Name'],
156                     number=artifact_count,
157                     artifact_type=artifact_type,
158                     a=min_,
159                     b=max_
160                 )
161                 matches.append(RuleMatch(
162                     path + [artifact_type],
163                     message
164                 ))
165         else:
166             if artifact_count != constraints[constraint_key]:
167                 message = (
168                     'Action "{action}" declares {number} {artifact_type} which is not the '
169                     'expected number [{a}].'
170                 ).format(
171                     action=action['Name'],
172                     number=artifact_count,
173                     artifact_type=artifact_type,
174                     a=constraints[constraint_key]
175                 )
176                 matches.append(RuleMatch(
177                     path + [artifact_type],
178                     message
179                 ))
180 
181         return matches
182 
183     def check_version(self, action, path):
184         """Check that action type version is valid."""
185         matches = []
186 
187         REGEX_VERSION_STRING = re.compile(r'^[0-9A-Za-z_-]+$')
188         LENGTH_MIN = 1
189         LENGTH_MAX = 9
190 
191         version = action.get('ActionTypeId', {}).get('Version')
192         if isinstance(version, dict):
193             self.logger.debug('Unable to validate version when an object is used.  Skipping')
194         elif isinstance(version, (six.string_types)):
195             if not LENGTH_MIN <= len(version) <= LENGTH_MAX:
196                 message = 'Version string ({0}) must be between {1} and {2} characters in length.'
197                 matches.append(RuleMatch(
198                     path + ['ActionTypeId', 'Version'],
199                     message.format(version, LENGTH_MIN, LENGTH_MAX)))
200             elif not re.match(REGEX_VERSION_STRING, version):
201                 message = 'Version string must match the pattern [0-9A-Za-z_-]+.'
202                 matches.append(RuleMatch(
203                     path + ['ActionTypeId', 'Version'],
204                     message
205                 ))
206         return matches
207 
208     def check_names_unique(self, action, path, action_names):
209         """Check that action names are unique."""
210         matches = []
211 
212         action_name = action.get('Name')
213         if isinstance(action_name, six.string_types):
214             if action.get('Name') in action_names:
215                 message = 'All action names within a stage must be unique. ({name})'.format(
216                     name=action.get('Name')
217                 )
218                 matches.append(RuleMatch(path + ['Name'], message))
219             action_names.add(action.get('Name'))
220 
221         return matches
222 
223     def match(self, cfn):
224         """Check that stage actions are set up properly."""
225         matches = []
226 
227         resources = cfn.get_resource_properties(['AWS::CodePipeline::Pipeline'])
228         for resource in resources:
229             path = resource['Path']
230             properties = resource['Value']
231 
232             s_stages = properties.get_safe('Stages', path)
233             for s_stage_v, s_stage_p in s_stages:
234                 if not isinstance(s_stage_v, list):
235                     self.logger.debug(
236                         'Stages not list. Should have been caught by generic linting.')
237                     return matches
238 
239                 for l_i_stage, l_i_path in s_stage_v.items_safe(s_stage_p):
240                     action_names = set()
241                     s_actions = l_i_stage.get_safe('Actions', l_i_path)
242                     for s_action_v, s_action_p in s_actions:
243                         if not isinstance(s_action_v, list):
244                             self.logger.debug(
245                                 'Actions not list. Should have been caught by generic linting.')
246                             return matches
247 
248                         for l_i_a_action, l_i_a_path in s_action_v.items_safe(s_action_p):
249                             try:
250                                 full_path = path + l_i_path + l_i_a_path
251                                 matches.extend(self.check_names_unique(
252                                     l_i_a_action, full_path, action_names))
253                                 matches.extend(self.check_version(l_i_a_action, full_path))
254                                 matches.extend(self.check_artifact_counts(
255                                     l_i_a_action, 'InputArtifacts', full_path))
256                                 matches.extend(self.check_artifact_counts(
257                                     l_i_a_action, 'OutputArtifacts', full_path))
258                             except AttributeError as err:
259                                 self.logger.debug('Got AttributeError. Should have been caught by generic linting. '
260                                                   'Ignoring the error here: %s', str(err))
261 
262         return matches
263
```
--- 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
@@ -220,6 +220,35 @@
 
         return matches
 
+    def check_artifact_names(self, action, path, artifact_names):
+        """Check that output artifact names are unique and inputs are from previous stage outputs."""
+        matches = []
+
+        input_artifacts = action.get('InputArtifacts')
+        if isinstance(input_artifacts, list):
+            for input_artifact in input_artifacts:
+                artifact_name = input_artifact.get('Name')
+                if isinstance(artifact_name, six.string_types):
+                    if not artifact_name in artifact_names:
+                        message = 'Every input artifact for an action must match the output artifact of an action earlier in the pipeline. ({name})'.format(
+                            name=artifact_name
+                        )
+                        matches.append(RuleMatch(path + ['InputArtifacts', 'Name'], message))
+
+        output_artifacts = action.get('OutputArtifacts')
+        if isinstance(output_artifacts, list):
+            for output_artifact in output_artifacts:
+                artifact_name = output_artifact.get('Name')
+                if isinstance(artifact_name, six.string_types):
+                    if artifact_name in artifact_names:
+                        message = 'Every output artifact in the pipeline must have a unique name. ({name})'.format(
+                            name=artifact_name
+                        )
+                        matches.append(RuleMatch(path + ['OutputArtifacts', 'Name'], message))
+                    artifact_names.add(artifact_name)
+
+        return matches
+
     def match(self, cfn):
         """Check that stage actions are set up properly."""
         matches = []
@@ -228,6 +257,7 @@
         for resource in resources:
             path = resource['Path']
             properties = resource['Value']
+            artifact_names = set()
 
             s_stages = properties.get_safe('Stages', path)
             for s_stage_v, s_stage_p in s_stages:
@@ -255,6 +285,8 @@
                                     l_i_a_action, 'InputArtifacts', full_path))
                                 matches.extend(self.check_artifact_counts(
                                     l_i_a_action, 'OutputArtifacts', full_path))
+                                matches.extend(self.check_artifact_names(
+                                    l_i_a_action, full_path, artifact_names))
                             except AttributeError as err:
                                 self.logger.debug('Got AttributeError. Should have been caught by generic linting. '
                                                   'Ignoring the error here: %s', str(err))

{"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@@ -220,6 +220,35 @@\n \n         return matches\n \n+    def check_artifact_names(self, action, path, artifact_names):\n+        \"\"\"Check that output artifact names are unique and inputs are from previous stage outputs.\"\"\"\n+        matches = []\n+\n+        input_artifacts = action.get('InputArtifacts')\n+        if isinstance(input_artifacts, list):\n+            for input_artifact in input_artifacts:\n+                artifact_name = input_artifact.get('Name')\n+                if isinstance(artifact_name, six.string_types):\n+                    if not artifact_name in artifact_names:\n+                        message = 'Every input artifact for an action must match the output artifact of an action earlier in the pipeline. ({name})'.format(\n+                            name=artifact_name\n+                        )\n+                        matches.append(RuleMatch(path + ['InputArtifacts', 'Name'], message))\n+\n+        output_artifacts = action.get('OutputArtifacts')\n+        if isinstance(output_artifacts, list):\n+            for output_artifact in output_artifacts:\n+                artifact_name = output_artifact.get('Name')\n+                if isinstance(artifact_name, six.string_types):\n+                    if artifact_name in artifact_names:\n+                        message = 'Every output artifact in the pipeline must have a unique name. ({name})'.format(\n+                            name=artifact_name\n+                        )\n+                        matches.append(RuleMatch(path + ['OutputArtifacts', 'Name'], message))\n+                    artifact_names.add(artifact_name)\n+\n+        return matches\n+\n     def match(self, cfn):\n         \"\"\"Check that stage actions are set up properly.\"\"\"\n         matches = []\n@@ -228,6 +257,7 @@\n         for resource in resources:\n             path = resource['Path']\n             properties = resource['Value']\n+            artifact_names = set()\n \n             s_stages = properties.get_safe('Stages', path)\n             for s_stage_v, s_stage_p in s_stages:\n@@ -255,6 +285,8 @@\n                                     l_i_a_action, 'InputArtifacts', full_path))\n                                 matches.extend(self.check_artifact_counts(\n                                     l_i_a_action, 'OutputArtifacts', full_path))\n+                                matches.extend(self.check_artifact_names(\n+                                    l_i_a_action, full_path, artifact_names))\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", "issue": "Doesn't catch CodePipeline OutputArtifacts need to be uniquely named\ncfn-lint 0.35.1\r\n\r\n*Description of issue.*\r\nThe linter doesn't catch that CodePipeline `OutputArtifacts` need to be uniquely named.\r\n\r\nPlease provide as much information as possible:\r\n* Template linting issues: \r\n  * Please provide a CloudFormation sample that generated the issue.\r\n\r\nThis template generates the error `UPDATE_FAILED | Output Artifact Bundle name must be unique within the pipeline. CreateOutput has been used more than once.`\r\n\r\n<details>\r\n\r\n```yaml\r\nAWSTemplateFormatVersion: \"2010-09-09\"\r\nDescription: The AWS CloudFormation template for this Serverless application\r\nResources:\r\n  ServerlessDeploymentPipeline:\r\n    Type: AWS::CodePipeline::Pipeline\r\n    Properties:\r\n      ArtifactStores:\r\n        - Region: ca-central-1\r\n          ArtifactStore:\r\n            Type: S3\r\n            Location: my-artifact-bucket\r\n      Name: my-code-pipeline\r\n      RestartExecutionOnUpdate: false\r\n      RoleArn: arn:aws:iam::000000000000:role/root\r\n      Stages:\r\n        - Name: Source\r\n          Actions:\r\n            - Name: SourceAction\r\n              ActionTypeId:\r\n                Category: Source\r\n                Owner: AWS\r\n                Version: \"1\"\r\n                Provider: S3\r\n              OutputArtifacts:\r\n                - Name: SourceArtifact\r\n              Configuration:\r\n                S3Bucket: my-source-bucket\r\n                S3ObjectKey: source-item.zip\r\n              RunOrder: 1\r\n        - Name: DeployToEnvA\r\n          Actions:\r\n            - Name: CreateChangeSetEnvA\r\n              Region: us-east-1\r\n              ActionTypeId:\r\n                Category: Deploy\r\n                Owner: AWS\r\n                Version: \"1\"\r\n                Provider: CloudFormation\r\n              InputArtifacts:\r\n                - Name: SourceArtifact\r\n              OutputArtifacts:\r\n                - Name: CreateOutput\r\n              Configuration:\r\n                ActionMode: CHANGE_SET_REPLACE\r\n                StackName: my-service-env-a\r\n                Capabilities: CAPABILITY_NAMED_IAM\r\n                RoleArn: arn:aws:iam::000000000000:role/root\r\n                TemplatePath: SourceArtifact::env-a-us-east-1.json\r\n                ChangeSetName: ChangeSet\r\n              RunOrder: 1\r\n              RoleArn: arn:aws:iam::000000000000:role/root\r\n            - Name: CreateChangeSetEnvB\r\n              Region: us-east-1\r\n              ActionTypeId:\r\n                Category: Deploy\r\n                Owner: AWS\r\n                Version: \"1\"\r\n                Provider: CloudFormation\r\n              InputArtifacts:\r\n                - Name: SourceArtifact\r\n              OutputArtifacts:\r\n                - Name: CreateOutput\r\n              Configuration:\r\n                ActionMode: CHANGE_SET_REPLACE\r\n                StackName: my-service-env-b\r\n                Capabilities: CAPABILITY_NAMED_IAM\r\n                RoleArn: arn:aws:iam::000000000000:role/root\r\n                TemplatePath: SourceArtifact::env-b-us-east-1.json\r\n                ChangeSetName: ChangeSet\r\n              RunOrder: 1\r\n              RoleArn: arn:aws:iam::000000000000:role/root\r\n\r\n```\r\n\r\n</details>\r\n\r\n  * If present, please add links to the (official) documentation for clarification.\r\n      - > Every output artifact in the pipeline must have a unique name.  \r\n\r\n          [Source](https://docs.aws.amazon.com/codepipeline/latest/userguide/welcome-introducing-artifacts.html)\r\n  * Validate if the issue still exists with the latest version of `cfn-lint` and/or the latest Spec files: :heavy_check_mark:  `0.35.1` is the latest version\r\n\r\n\r\n\r\nCfn-lint uses the [CloudFormation Resource Specifications](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) as the base to do validation. These files are included as part of the application version. Please update to the latest version of `cfn-lint` or update the spec files manually (`cfn-lint -u`)\r\n\r\n:heavy_check_mark:  I have also tried after running `cfn-lint -u`\n", "before_files": [{"content": "\"\"\"\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: MIT-0\n\"\"\"\nimport re\nimport six\nfrom cfnlint.rules import CloudFormationLintRule\nfrom cfnlint.rules 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    CONSTRAINTS = {\n        'AWS': {\n            'Source': {\n                'S3': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'CodeCommit': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'ECR': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n            'Test': {\n                'CodeBuild': {\n                    'InputArtifactRange': (1, 5),\n                    'OutputArtifactRange': (0, 5),\n                },\n                'DeviceFarm': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Build': {\n                'CodeBuild': {\n                    'InputArtifactRange': (1, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            },\n            'Approval': {\n                'Manual': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Deploy': {\n                'S3': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\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                'ServiceCatalog': {\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            'Deploy': {\n                'AlexaSkillsKit': {\n                    'InputArtifactRange': (0, 2),\n                    'OutputArtifactRange': 0,\n                },\n            },\n        },\n        'Custom': {\n            'Build': {\n                'Jenkins': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\n                },\n            },\n            'Test': {\n                'Jenkins': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\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_version(self, action, path):\n        \"\"\"Check that action type version is valid.\"\"\"\n        matches = []\n\n        REGEX_VERSION_STRING = re.compile(r'^[0-9A-Za-z_-]+$')\n        LENGTH_MIN = 1\n        LENGTH_MAX = 9\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 isinstance(version, (six.string_types)):\n            if not LENGTH_MIN <= len(version) <= LENGTH_MAX:\n                message = 'Version string ({0}) must be between {1} and {2} characters in length.'\n                matches.append(RuleMatch(\n                    path + ['ActionTypeId', 'Version'],\n                    message.format(version, LENGTH_MIN, LENGTH_MAX)))\n            elif not re.match(REGEX_VERSION_STRING, version):\n                message = 'Version string must match the pattern [0-9A-Za-z_-]+.'\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        action_name = action.get('Name')\n        if isinstance(action_name, six.string_types):\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(\n                        '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(\n                                '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                                full_path = path + l_i_path + l_i_a_path\n                                matches.extend(self.check_names_unique(\n                                    l_i_a_action, full_path, action_names))\n                                matches.extend(self.check_version(l_i_a_action, full_path))\n                                matches.extend(self.check_artifact_counts(\n                                    l_i_a_action, 'InputArtifacts', full_path))\n                                matches.extend(self.check_artifact_counts(\n                                    l_i_a_action, 'OutputArtifacts', full_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": "\"\"\"\nCopyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: MIT-0\n\"\"\"\nimport re\nimport six\nfrom cfnlint.rules import CloudFormationLintRule\nfrom cfnlint.rules 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    CONSTRAINTS = {\n        'AWS': {\n            'Source': {\n                'S3': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'CodeCommit': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                },\n                'ECR': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 1,\n                }\n            },\n            'Test': {\n                'CodeBuild': {\n                    'InputArtifactRange': (1, 5),\n                    'OutputArtifactRange': (0, 5),\n                },\n                'DeviceFarm': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Build': {\n                'CodeBuild': {\n                    'InputArtifactRange': (1, 5),\n                    'OutputArtifactRange': (0, 5),\n                }\n            },\n            'Approval': {\n                'Manual': {\n                    'InputArtifactRange': 0,\n                    'OutputArtifactRange': 0,\n                }\n            },\n            'Deploy': {\n                'S3': {\n                    'InputArtifactRange': 1,\n                    'OutputArtifactRange': 0,\n                },\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                'ServiceCatalog': {\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            'Deploy': {\n                'AlexaSkillsKit': {\n                    'InputArtifactRange': (0, 2),\n                    'OutputArtifactRange': 0,\n                },\n            },\n        },\n        'Custom': {\n            'Build': {\n                'Jenkins': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\n                },\n            },\n            'Test': {\n                'Jenkins': {\n                    'InputArtifactRange': (0, 5),\n                    'OutputArtifactRange': (0, 5),\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_version(self, action, path):\n        \"\"\"Check that action type version is valid.\"\"\"\n        matches = []\n\n        REGEX_VERSION_STRING = re.compile(r'^[0-9A-Za-z_-]+$')\n        LENGTH_MIN = 1\n        LENGTH_MAX = 9\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 isinstance(version, (six.string_types)):\n            if not LENGTH_MIN <= len(version) <= LENGTH_MAX:\n                message = 'Version string ({0}) must be between {1} and {2} characters in length.'\n                matches.append(RuleMatch(\n                    path + ['ActionTypeId', 'Version'],\n                    message.format(version, LENGTH_MIN, LENGTH_MAX)))\n            elif not re.match(REGEX_VERSION_STRING, version):\n                message = 'Version string must match the pattern [0-9A-Za-z_-]+.'\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        action_name = action.get('Name')\n        if isinstance(action_name, six.string_types):\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 check_artifact_names(self, action, path, artifact_names):\n        \"\"\"Check that output artifact names are unique and inputs are from previous stage outputs.\"\"\"\n        matches = []\n\n        input_artifacts = action.get('InputArtifacts')\n        if isinstance(input_artifacts, list):\n            for input_artifact in input_artifacts:\n                artifact_name = input_artifact.get('Name')\n                if isinstance(artifact_name, six.string_types):\n                    if not artifact_name in artifact_names:\n                        message = 'Every input artifact for an action must match the output artifact of an action earlier in the pipeline. ({name})'.format(\n                            name=artifact_name\n                        )\n                        matches.append(RuleMatch(path + ['InputArtifacts', 'Name'], message))\n\n        output_artifacts = action.get('OutputArtifacts')\n        if isinstance(output_artifacts, list):\n            for output_artifact in output_artifacts:\n                artifact_name = output_artifact.get('Name')\n                if isinstance(artifact_name, six.string_types):\n                    if artifact_name in artifact_names:\n                        message = 'Every output artifact in the pipeline must have a unique name. ({name})'.format(\n                            name=artifact_name\n                        )\n                        matches.append(RuleMatch(path + ['OutputArtifacts', 'Name'], message))\n                    artifact_names.add(artifact_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            artifact_names = set()\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(\n                        '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(\n                                '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                                full_path = path + l_i_path + l_i_a_path\n                                matches.extend(self.check_names_unique(\n                                    l_i_a_action, full_path, action_names))\n                                matches.extend(self.check_version(l_i_a_action, full_path))\n                                matches.extend(self.check_artifact_counts(\n                                    l_i_a_action, 'InputArtifacts', full_path))\n                                matches.extend(self.check_artifact_counts(\n                                    l_i_a_action, 'OutputArtifacts', full_path))\n                                matches.extend(self.check_artifact_names(\n                                    l_i_a_action, full_path, artifact_names))\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"}]}