Skip to content

Commit 8cbfa6e

Browse files
authored
Merge branch 'main' into update-sfdc-api-v63.0
2 parents 9722c3f + a901956 commit 8cbfa6e

33 files changed

Lines changed: 745 additions & 321 deletions

.github/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ changelog:
77
authors:
88
- app/github-actions
99
categories:
10-
- title: Critical Changes 🛠
10+
- title: Critical Changes
1111
labels:
1212
- critical-change
13-
- title: Changes 🎉
13+
- title: Changes
1414
labels:
1515
- enhancement
1616
- "*"
1717
exclude:
1818
labels:
1919
- bug
20-
- title: Issues Fixed 🩴
20+
- title: Issues Fixed
2121
labels:
2222
- bug

.github/workflows/release_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
pip uninstall -y cumulusci
3535
- name: Store artifacts
3636
if: failure()
37-
uses: actions/upload-artifact@v3
37+
uses: actions/upload-artifact@v4
3838
with:
3939
name: packages
4040
path: dist

.github/workflows/release_test_sfdx.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ jobs:
4545
uses: actions/setup-python@v5
4646
with:
4747
python-version: 3.11
48-
cache: pip
49-
cache-dependency-path: "pyproject.toml"
5048
- name: Set up uv
5149
uses: SFDO-Tooling/setup-uv@main
5250
with:

.readthedocs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ sphinx:
2727
formats:
2828
- pdf
2929
- epub
30+
# Optionally declare the Python requirements required to build your docs
31+
# python:
32+
# install:
33+
# - requirements: requirements_dev.txt

cumulusci/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.3.0"
1+
__version__ = "4.3.0.dev0"

cumulusci/cumulusci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ tasks:
317317
description: Retrieves a list of the currently available license definition keys
318318
class_path: cumulusci.tasks.preflight.licenses.GetAvailableLicenses
319319
group: Salesforce Preflight Checks
320+
get_assignable_licenses:
321+
description: Retrieves a list of the currently assignable license definition keys based on unused licenses
322+
class_path: cumulusci.tasks.preflight.licenses.GetAssignableLicenses
323+
group: Salesforce Preflight Checks
320324
get_available_permission_set_licenses:
321325
description: Retrieves a list of the currently available Permission Set License definition keys
322326
class_path: cumulusci.tasks.preflight.licenses.GetAvailablePermissionSetLicenses
@@ -333,6 +337,10 @@ tasks:
333337
description: Retrieves a list of the currently available Permission Sets
334338
class_path: cumulusci.tasks.preflight.licenses.GetAvailablePermissionSets
335339
group: Salesforce Preflight Checks
340+
get_assignable_permission_sets:
341+
description: Retrieves a list of the currently assignable Permission Sets based on unused associated user licenses
342+
class_path: cumulusci.tasks.preflight.licenses.GetAssignablePermissionSets
343+
group: Salesforce Preflight Checks
336344
get_existing_record_types:
337345
description: "Retrieves all Record Types in the org as a dict, with sObject names as keys and lists of Developer Names as values."
338346
class_path: cumulusci.tasks.preflight.recordtypes.CheckSObjectRecordTypes

cumulusci/tasks/metadata/package.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,19 @@ def process_common_components(response_messages: List, components: Dict):
4545
"""Compare compoents in the api responce object with list of components and return common common components"""
4646
if not response_messages or not components:
4747
return components
48-
4948
for message in response_messages:
49+
5050
message_list = message.firstChild.nextSibling.firstChild.nodeValue.split("'")
5151
if len(message_list) > 1:
5252
component_type = message_list[1]
5353
message_txt = message_list[2]
54-
5554
if "is not available in this organization" in message_txt:
5655
del components[component_type]
56+
elif "is unknown" in message_txt:
57+
component_type = message_list[0].split(" ")
58+
components[component_type[0]].remove(message_list[1])
59+
if len(components[component_type]) == 0:
60+
del components[component_type]
5761
else:
5862
component_name = message_list[3]
5963
if component_name in components[component_type]:

cumulusci/tasks/preflight/licenses.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
11
from cumulusci.tasks.salesforce import BaseSalesforceApiTask
22

33

4-
class GetAvailableLicenses(BaseSalesforceApiTask):
4+
class BaseUserLicenseAwareTask(BaseSalesforceApiTask):
5+
def get_available_user_licenses(self, is_assignable=False):
6+
"""Fetch active user licenses with availability."""
7+
query = "SELECT Id, LicenseDefinitionKey, TotalLicenses, UsedLicenses FROM UserLicense WHERE Status = 'Active'"
8+
return {
9+
lic["Id"]: lic
10+
for lic in self.sf.query(query)["records"]
11+
if not is_assignable or (lic["TotalLicenses"] > lic["UsedLicenses"])
12+
}
13+
14+
def _log_list(self, title, items):
15+
self.logger.info(
16+
f"{title} ({len(items)}):\n" + "\n".join(f"- {item}" for item in items)
17+
)
18+
19+
20+
class GetAvailableLicenses(BaseUserLicenseAwareTask):
521
def _run_task(self):
622
self.return_values = [
723
result["LicenseDefinitionKey"]
8-
for result in self.sf.query("SELECT LicenseDefinitionKey FROM UserLicense")[
9-
"records"
10-
]
24+
for result in self.get_available_user_licenses().values()
1125
]
1226
licenses = "\n".join(self.return_values)
1327
self.logger.info(f"Found licenses:\n{licenses}")
1428

1529

30+
class GetAssignableLicenses(BaseUserLicenseAwareTask):
31+
def _run_task(self):
32+
self.return_values = [
33+
result["LicenseDefinitionKey"]
34+
for result in self.get_available_user_licenses(is_assignable=True).values()
35+
]
36+
licenses = "\n".join(self.return_values)
37+
self.logger.info(f"Found assignable licenses:\n{licenses}")
38+
39+
1640
class GetAvailablePermissionSetLicenses(BaseSalesforceApiTask):
1741
def _run_task(self):
42+
query = "SELECT PermissionSetLicenseKey FROM PermissionSetLicense WHERE Status = 'Active'"
1843
self.return_values = [
1944
result["PermissionSetLicenseKey"]
20-
for result in self.sf.query(
21-
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense"
22-
)["records"]
45+
for result in self.sf.query(query)["records"]
2346
]
2447
licenses = "\n".join(self.return_values)
2548
self.logger.info(f"Found permission set licenses:\n{licenses}")
@@ -44,3 +67,20 @@ def _run_task(self):
4467
]
4568
permsets = "\n".join(self.return_values)
4669
self.logger.info(f"Found Permission Sets:\n{permsets}")
70+
71+
72+
class GetAssignablePermissionSets(BaseUserLicenseAwareTask):
73+
def _run_task(self):
74+
license_data = self.get_available_user_licenses(is_assignable=True)
75+
permsets = self.sf.query_all("SELECT LicenseId, Name FROM PermissionSet")[
76+
"records"
77+
]
78+
available_permsets = [
79+
ps["Name"]
80+
for ps in permsets
81+
if not ps["LicenseId"] or ps["LicenseId"] in license_data
82+
]
83+
84+
self.return_values = available_permsets
85+
permsets = "\n".join(self.return_values)
86+
self.logger.info(f"Found assignable permission sets:\n{permsets}")

cumulusci/tasks/preflight/tests/test_licenses.py

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from unittest.mock import Mock
22

33
from cumulusci.tasks.preflight.licenses import (
4+
GetAssignableLicenses,
5+
GetAssignablePermissionSets,
46
GetAvailableLicenses,
57
GetAvailablePermissionSetLicenses,
68
GetAvailablePermissionSets,
@@ -16,17 +18,56 @@ def test_license_preflight(self):
1618
task._init_api.return_value.query.return_value = {
1719
"totalSize": 2,
1820
"records": [
19-
{"LicenseDefinitionKey": "TEST1"},
20-
{"LicenseDefinitionKey": "TEST2"},
21+
{
22+
"Id": "L1",
23+
"LicenseDefinitionKey": "TEST1",
24+
"TotalLicenses": 100,
25+
"UsedLicenses": 90,
26+
},
27+
{
28+
"Id": "L2",
29+
"LicenseDefinitionKey": "TEST2",
30+
"TotalLicenses": 100,
31+
"UsedLicenses": 100,
32+
},
2133
],
2234
}
23-
task()
2435

36+
task()
2537
task._init_api.return_value.query.assert_called_once_with(
26-
"SELECT LicenseDefinitionKey FROM UserLicense"
38+
"SELECT Id, LicenseDefinitionKey, TotalLicenses, UsedLicenses FROM UserLicense WHERE Status = 'Active'"
2739
)
40+
2841
assert task.return_values == ["TEST1", "TEST2"]
2942

43+
def test_assignable_license_preflight(self):
44+
task = create_task(GetAssignableLicenses, {})
45+
task._init_api = Mock()
46+
task._init_api.return_value.query.return_value = {
47+
"totalSize": 2,
48+
"records": [
49+
{
50+
"Id": "L1",
51+
"LicenseDefinitionKey": "TEST1",
52+
"TotalLicenses": 100,
53+
"UsedLicenses": 90,
54+
},
55+
{
56+
"Id": "L2",
57+
"LicenseDefinitionKey": "TEST2",
58+
"TotalLicenses": 100,
59+
"UsedLicenses": 100,
60+
},
61+
],
62+
}
63+
64+
task()
65+
task._init_api.return_value.query.assert_called_once_with(
66+
"SELECT Id, LicenseDefinitionKey, TotalLicenses, UsedLicenses FROM UserLicense WHERE Status = 'Active'"
67+
)
68+
# Only TEST1 assignable licenses
69+
assert task.return_values == ["TEST1"]
70+
3071
def test_psl_preflight(self):
3172
task = create_task(GetAvailablePermissionSetLicenses, {})
3273
task._init_api = Mock()
@@ -41,7 +82,7 @@ def test_psl_preflight(self):
4182
task()
4283

4384
task._init_api.return_value.query.assert_called_once_with(
44-
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense"
85+
"SELECT PermissionSetLicenseKey FROM PermissionSetLicense WHERE Status = 'Active'"
4586
)
4687
assert task.return_values == ["TEST1", "TEST2"]
4788

@@ -93,3 +134,41 @@ def test_permsets_preflight(self):
93134
"SELECT Name FROM PermissionSet"
94135
)
95136
assert task.return_values == ["TEST1", "TEST2"]
137+
138+
def test_assignable_permsets_preflight(self):
139+
task = create_task(GetAssignablePermissionSets, {})
140+
task._init_api = Mock()
141+
task._init_api.return_value.query.return_value = {
142+
"totalSize": 2,
143+
"records": [
144+
{
145+
"Id": "L1",
146+
"LicenseDefinitionKey": "TEST1",
147+
"TotalLicenses": 100,
148+
"UsedLicenses": 90,
149+
},
150+
{
151+
"Id": "L2",
152+
"LicenseDefinitionKey": "TEST2",
153+
"TotalLicenses": 100,
154+
"UsedLicenses": 100,
155+
},
156+
],
157+
}
158+
task._init_api.return_value.query_all.return_value = {
159+
"totalSize": 3,
160+
"records": [
161+
{"LicenseId": "L1", "Name": "TEST1"},
162+
{"LicenseId": "L2", "Name": "TEST2"},
163+
{"LicenseId": None, "Name": "TEST3"},
164+
],
165+
}
166+
task()
167+
168+
task._init_api.return_value.query.assert_called_once_with(
169+
"SELECT Id, LicenseDefinitionKey, TotalLicenses, UsedLicenses FROM UserLicense WHERE Status = 'Active'"
170+
)
171+
task._init_api.return_value.query_all.assert_called_once_with(
172+
"SELECT LicenseId, Name FROM PermissionSet"
173+
)
174+
assert task.return_values == ["TEST1", "TEST3"]

0 commit comments

Comments
 (0)