Skip to content

Commit 6219efa

Browse files
Merge branch 'main' into @W-18161882-Unppinning-RobotFramework
2 parents 4e295a7 + ff5db19 commit 6219efa

29 files changed

Lines changed: 397 additions & 239 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/cumulusci.yml

Lines changed: 9 additions & 1 deletion
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
@@ -1488,7 +1496,7 @@ project:
14881496
namespace:
14891497
install_class:
14901498
uninstall_class:
1491-
api_version: "62.0"
1499+
api_version: "63.0"
14921500
git:
14931501
default_branch: master
14941502
prefix_feature: feature/

cumulusci/tasks/preflight/licenses.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
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):
1842
query = "SELECT PermissionSetLicenseKey FROM PermissionSetLicense WHERE Status = 'Active'"
@@ -43,3 +67,20 @@ def _run_task(self):
4367
]
4468
permsets = "\n".join(self.return_values)
4569
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: 83 additions & 4 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()
@@ -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"]

docs/about.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
```{toctree}
44
:maxdepth: 1
55
6-
history
76
contributing
8-
```
7+
history
8+
```

docs/conf.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@
100100
# directories to ignore when looking for source files.
101101
exclude_patterns = ["_build", "*/tests/"]
102102

103+
# Exclude 'history.md' only in LaTeX builds
104+
if os.environ.get("SPHINX_OUTPUT_FORMAT") == "latex":
105+
exclude_patterns.append("history.md")
106+
103107
# The reST default role (used for this markup: `text`) to use for all
104108
# documents.
105109
# default_role = None
@@ -222,21 +226,22 @@
222226
# This is the file name suffix for HTML files (e.g. ".xhtml").
223227
# html_file_suffix = None
224228

229+
suppress_warnings = ["ref.duplicate_label"]
230+
225231
# Output file base name for HTML help builder.
226232
htmlhelp_basename = "cumuluscidoc"
227233

228234

229235
# -- Options for LaTeX output ------------------------------------------
236+
latex_engine = "xelatex"
237+
230238

231239
latex_elements = {
232-
# The paper size ('letterpaper' or 'a4paper').
233-
# 'papersize': 'letterpaper',
234-
# The font size ('10pt', '11pt' or '12pt').
235-
# 'pointsize': '10pt',
236-
# Additional stuff for the LaTeX preamble.
237-
# 'preamble': '',
240+
'preamble': r'''\renewcommand{\familydefault}{\sfdefault} % Set default font to sans-serif
241+
''',
238242
}
239243

244+
240245
# Grouping the document tree into LaTeX files. List of tuples
241246
# (source start file, target name, title, author, documentclass
242247
# [howto/manual]).
@@ -245,13 +250,16 @@
245250
"index",
246251
"cumulusci.tex",
247252
"CumulusCI Documentation",
248-
"""Kamalnath Devarakonda\\\\
249-
Sr Director Software Engineering\\\\
250-
kdevarakonda@salesforce.com""",
253+
r"""
254+
\parbox{\textwidth}{\raggedleft
255+
{\fontsize{15}{12} Kamalnath Devarakonda}\\
256+
{\fontsize{14}{12}\selectfont Sr. Director, Software Engineering}\\
257+
{\fontsize{13}{12}\selectfont kdevarakonda@salesforce.com}
258+
}
259+
""",
251260
"manual",
252261
)
253262
]
254-
255263
# The name of an image file (relative to this directory) to place at
256264
# the top of the title page.
257265
# latex_logo = None

docs/config.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Options
8686
Require at least X percent code coverage across the org following the test run.
8787
Default: 90
8888
```
89-
89+
(add-a-custom-task)=
9090
### Add a Custom Task
9191

9292
To define a new task for your project, add the task name under the
@@ -534,7 +534,7 @@ will fetch the most recent production release, or the default branch if
534534
there are no releases. By specifying `resolution_strategy`, the behavior
535535
can be changed to match desired dependency resolution behavior, such as
536536
using beta releases or retrieving feature test packages from a commit
537-
status. See [](dependency-resolution) for
537+
status. See [](controlling-github-dependency-resolution) for
538538
more details about resolution strategies.
539539

540540
```{note}
@@ -723,7 +723,7 @@ in your cumulusci.yml file.
723723

724724
### Customizing Metadata Deployment
725725

726-
CumulusCI's `deploy` task offers deep flexibility to customize your deployment process. Review [](deploy) for an in-depth guide.
726+
CumulusCI's `deploy` task offers deep flexibility to customize your deployment process. Review [](configure-metadata-deployment) for an in-depth guide.
727727

728728
```{toctree}
729729
---

0 commit comments

Comments
 (0)