Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check_components preflight to detect metadata component conflicts (#3837) #3837

Merged
merged 19 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cumulusci/cumulusci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ tasks:
description: Waits on a batch apex or queueable apex job to finish.
class_path: cumulusci.tasks.apex.batch.BatchApexWait
group: Salesforce
check_components:
description: "Check if common components exist in the target org based on provided deploy paths or those from a plan/flow."
class_path: cumulusci.tasks.salesforce.check_components.CheckComponents
group: Salesforce Preflight Checks
check_dataset_load:
description: Runs as a preflight check to determine whether dataset can be loaded successfully.
class_path: cumulusci.tasks.preflight.dataset_load.LoadDataSetCheck
Expand Down
23 changes: 23 additions & 0 deletions cumulusci/tasks/metadata/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import urllib.parse
from logging import Logger, getLogger
from pathlib import Path
from typing import Dict, List

import yaml

Expand Down Expand Up @@ -40,6 +41,28 @@ def metadata_sort_key_section(name):
return key


def process_common_components(response_messages: List, components: Dict):
"""Compare compoents in the api responce object with list of components and return common common components"""
if not response_messages or not components:
return components

for message in response_messages:
message_list = message.firstChild.nextSibling.firstChild.nodeValue.split("'")
if len(message_list) > 1:
component_type = message_list[1]
message_txt = message_list[2]

if "is not available in this organization" in message_txt:
del components[component_type]
else:
component_name = message_list[3]
if component_name in components[component_type]:
components[component_type].remove(component_name)
if len(components[component_type]) == 0:
del components[component_type]
return components


class MetadataParserMissingError(Exception):
pass

Expand Down
76 changes: 76 additions & 0 deletions cumulusci/tasks/metadata/tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unittest import mock

import pytest
from defusedxml.minidom import parseString

from cumulusci.core.config import (
BaseProjectConfig,
Expand All @@ -27,6 +28,7 @@
RecordTypeParser,
UpdatePackageXml,
metadata_sort_key,
process_common_components,
)
from cumulusci.utils import temporary_dir, touch

Expand Down Expand Up @@ -398,3 +400,77 @@ def test_run_task(self):
with open(output_path, "r") as f:
result = f.read()
assert expected == result


class TestProcessComponents:
response = """<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns="http://soap.sforce.com/2006/04/metadata">
<soapenv:Body> <checkRetrieveStatusResponse>
<result>
<done>true</done>
<fileProperties>
<createdById>0058N000006PycGQAS</createdById>
<createdByName>User User</createdByName>
<createdDate>2024-10-08T22:54:34.372Z</createdDate>
<fileName>unpackaged/labels/CustomLabels.labels</fileName>
<fullName>CustomLabels</fullName>
<id>000000000000000AAA</id>
<lastModifiedById>0058N000006PycGQAS</lastModifiedById>
<lastModifiedByName>User User</lastModifiedByName>
<lastModifiedDate>2024-10-08T22:54:34.372Z</lastModifiedDate>
<type>CustomLabels</type>
</fileProperties>
<id>09S8N000002vlujUAA</id>
<messages>
<problem>Entity of type 'ApexClass' 'TestClass' cannot be found</problem>
<fileName>unpackaged/package.xml</fileName>
</messages>
<messages>
<problem>Entity of type 'CustomObject' 'TestObject' cannot be found</problem>
<fileName>unpackaged/package.xml</fileName>
</messages>
<messages>
<problem>Entity of type 'CustomObject' 'AnotherObject' cannot be found</problem>
<fileName>unpackaged/package.xml</fileName>
</messages>
</result></checkRetrieveStatusResponse></soapenv:Body></soapenv:Envelope>
"""

def test_process_common_components(self):

response_messages = parseString(self.response).getElementsByTagName("messages")

components = {
"ApexClass": {"TestClass", "AnotherClass"},
"CustomObject": {"TestObject", "AnotherObject"},
}

result = process_common_components(response_messages, components)

expected_components = {
"ApexClass": {"AnotherClass"},
}

assert result == expected_components
assert "ApexClass" in result
assert "AnotherClass" in result["ApexClass"]
assert "TestClass" not in result["ApexClass"]
assert "CustomObject" not in result

def test_process_common_components_no_response_messages(self):
components = {
"ApexClass": {"TestClass", "AnotherClass"},
"CustomObject": {"TestObject", "AnotherObject"},
}

result = process_common_components([], components)

# If there are no response messages, the components list should remain unchanged
assert result == components

def test_process_common_components_no_components(self):
response_messages = parseString(self.response).getElementsByTagName("messages")
result = process_common_components(response_messages, {})
assert result == {}
24 changes: 4 additions & 20 deletions cumulusci/tasks/salesforce/Deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from cumulusci.salesforce_api.metadata import ApiDeploy, ApiRetrieveUnpackaged
from cumulusci.salesforce_api.package_zip import MetadataPackageZipBuilder
from cumulusci.salesforce_api.rest_deploy import RestDeploy
from cumulusci.tasks.metadata.package import process_common_components
from cumulusci.tasks.salesforce.BaseSalesforceMetadataApiTask import (
BaseSalesforceMetadataApiTask,
)
Expand Down Expand Up @@ -169,38 +170,21 @@ def _create_api_object(self, package_xml, api_version):
return api_retrieve_unpackaged_object

def _collision_check(self, src_path):
xml_map = {}
is_collision = False
package_xml = open(f"{src_path}/package.xml", "r")
source_xml_tree = metadata_tree.parse(f"{src_path}/package.xml")

for type in source_xml_tree.types:
members = []
try:
for member in type.members:
members.append(member.text)
except AttributeError: # Exception if there are no members for a type
pass
xml_map[type["name"].text] = members

api_retrieve_unpackaged_response = self._create_api_object(
package_xml.read(), source_xml_tree.version.text
)

xml_map = metadata_tree.parse_package_xml_types("name", source_xml_tree)

messages = parseString(
api_retrieve_unpackaged_response._get_response().content
).getElementsByTagName("messages")

for i in range(len(messages)):
# print(messages[i])
message_list = messages[
i
].firstChild.nextSibling.firstChild.nodeValue.split("'")

if message_list[3] in xml_map[message_list[1]]:
xml_map[message_list[1]].remove(message_list[3])
if len(xml_map[message_list[1]]) == 0:
del xml_map[message_list[1]]
process_common_components(messages, xml_map)

for type, api_names in xml_map.items():
if len(api_names) != 0:
Expand Down
Loading
Loading