From 369dc0fb45a615a6892c68f56f103cbb3b15301e Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Tue, 19 Dec 2023 12:54:38 -0600 Subject: [PATCH 1/3] Added title, description, and examples to all pydantic models for the cumulusci.yml json schema to provide better help information in IDEs. Currently the test to compare the generated schema to the schema file is failing for reasons I can't understand yet, but the content is ready for review. --- cumulusci/schema/cumulusci.jsonschema.json | 1010 ++++++++++++++- cumulusci/utils/yaml/cumulusci_yml.py | 1294 ++++++++++++++++++-- 2 files changed, 2135 insertions(+), 169 deletions(-) diff --git a/cumulusci/schema/cumulusci.jsonschema.json b/cumulusci/schema/cumulusci.jsonschema.json index 255d2de4d4..db90c74f5a 100644 --- a/cumulusci/schema/cumulusci.jsonschema.json +++ b/cumulusci/schema/cumulusci.jsonschema.json @@ -1,10 +1,31 @@ { - "title": "CumulusCIRoot", + "title": "CumulusCI Project Configuration", "type": "object", "properties": { "tasks": { "title": "Tasks", + "description": "A dictionary of tasks for the project.", "default": {}, + "examples": [ + { + "deploy": { + "class_path": "cumulusci.tasks.salesforce.Deploy", + "description": "Deploys the metadata in the project to the org.", + "group": "Deployment", + "options": { + "path": "src" + } + }, + "load_custom_settings": { + "class_path": "cumulusci.tasks.salesforce.LoadCustomSettings", + "description": "Loads custom settings data into the org.", + "group": "Data", + "options": { + "path": "data/custom_settings" + } + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/Task" @@ -12,7 +33,48 @@ }, "flows": { "title": "Flows", + "description": "A dictionary of flows for the project.", "default": {}, + "examples": [ + { + "dependencies": { + "description": "Installs all dependencies for the project.", + "steps": { + "1": { + "task": "update_dependencies" + }, + "2": { + "task": "install_prod" + } + } + }, + "config_dev": { + "description": "Configures the org for development.", + "steps": { + "1": { + "flow": "config_common" + }, + "2": { + "flow": "config_dev" + } + } + }, + "dev_org": { + "description": "Sets up a new dev org.", + "steps": { + "1": { + "flow": "dependencies" + }, + "2": { + "flow": "deploy_unmanaged" + }, + "3": { + "flow": "config_dev" + } + } + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/Flow" @@ -20,7 +82,52 @@ }, "project": { "title": "Project", + "description": "The project configuration.", "default": {}, + "examples": [ + { + "name": "Nonprofit Success Pack", + "package": { + "name": "Nonprofit Success Pack", + "namespace": "npsp", + "api_version": "59.0" + }, + "test": { + "name_match": "%Test" + }, + "git": { + "repo_url": "https://github.com/SalesforceFoundation/NPSP", + "default_branch": "main", + "prefix_feature": "feature/", + "prefix_beta": "beta/", + "prefix_release": "release/", + "push_prefix_sandbox": "sandbox/", + "push_prefix_production": "prod/" + }, + "dependencies": [ + { + "namespace": "somens", + "version": "1.23" + }, + { + "github": "https://github.com/SalesforceFoundation/Households" + } + ], + "dependency_resolutions": { + "production": "latest_release", + "preproduction": "include_beta", + "resolution_strategies": { + "latest_release": ["tag", "latest_release", "unmanaged"], + "include_beta": [ + "tag", + "latest_beta", + "latest_release", + "unmanaged" + ] + } + } + } + ], "allOf": [ { "$ref": "#/definitions/Project" @@ -29,7 +136,30 @@ }, "orgs": { "title": "Orgs", + "description": "A dictionary of org configurations for the project.", "default": {}, + "examples": [ + { + "scratch": { + "dev": { + "config_file": "orgs/dev.json", + "days": 1, + "namespaced": true, + "setup_flow": "dev_org", + "noancestors": true, + "release": "preview" + }, + "qa": { + "config_file": "orgs/qa.json", + "days": 7, + "namespaced": true, + "setup_flow": "qa_org", + "noancestors": true, + "release": "previous" + } + } + } + ], "allOf": [ { "$ref": "#/definitions/Orgs" @@ -38,30 +168,209 @@ }, "services": { "title": "Services", + "description": "A dictionary of services for the project.", "default": {}, + "examples": [ + { + "github": { + "description": "Create a connection to GitHub to interact with the API.", + "attributes": { + "username": { + "description": "The username for the service.", + "required": true, + "default_factory": "cumulusci.core.config.OrgConfig.default_username" + }, + "password": { + "description": "The password for the service.", + "required": true, + "sensitive": true + } + }, + "validator": "cumulusci.core.github.validate_service" + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/Service" } }, "cumulusci": { - "$ref": "#/definitions/CumulusCIConfig" + "title": "CumulusCI", + "description": "The CumulusCI configuration.", + "examples": [ + { + "keychain": "cumulusci.core.keychain.EnvironmentProjectKeychain" + } + ], + "allOf": [ + { + "$ref": "#/definitions/CumulusCIConfig" + } + ] }, "plans": { "title": "Plans", + "description": "A dictionary of plans for the project.", "default": {}, + "examples": [ + { + "base_install": { + "title": "Base Install", + "description": "A base install of the packages only", + "tier": "primary", + "slug": "base-install", + "is_listed": true, + "steps": { + "1": { + "task": "update_dependencies" + }, + "2": { + "task": "install_prod" + } + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org." + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org." + } + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"] + }, + "full_demo_install": { + "title": "Full Demo Install", + "description": "A full demo install of the packages with configuration and sample data", + "tier": "secondary", + "slug": "full-demo-install", + "is_listed": true, + "steps": { + "1": { + "task": "update_dependencies" + }, + "2": { + "task": "install_prod" + }, + "3": { + "flow": "config_common" + }, + "4": { + "flow": "config_demo" + }, + "5": { + "flow": "demo_data" + } + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org." + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org." + } + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"] + }, + "customer_org": { + "title": "Customer Org", + "description": "A customer org with the packages installed", + "tier": "additional", + "slug": "customer-org", + "is_listed": true, + "steps": { + "1": { + "flow": "customer_org" + } + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org." + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org." + } + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"] + }, + "customer_org_full": { + "title": "Customer Org Full", + "description": "A customer org with the packages installed and configured", + "tier": "additional", + "slug": "customer-org-full", + "is_listed": true, + "steps": { + "1": { + "task": "customer_org_full" + } + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org." + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org." + } + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"] + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/Plan" } }, "minimum_cumulusci_version": { - "title": "Minimum Cumulusci Version", + "title": "Minimum CumulusCI Version", + "description": "The minimum version of CumulusCI required to run the project.", + "examples": ["3.0.0"], "type": "string" }, "sources": { "title": "Sources", + "description": "A dictionary of sources for the project.", "default": {}, + "examples": [ + { + "npsp": { + "github": "https://github.com/SalesforceFoundation/NPSP", + "resolution_strategy": "latest_release" + }, + "pmm": { + "github": "https://github.com/SalesforceFoundation/PMM", + "resolution_strategy": "latest_release" + } + } + ], "type": "object", "additionalProperties": { "anyOf": [ @@ -75,38 +384,104 @@ } }, "cli": { - "$ref": "#/definitions/CumulusCLIConfig" + "title": "CumulusCI CLI", + "description": "The CumulusCI CLI configuration.", + "examples": [ + { + "show_stacktraces": false, + "plain_output": false + } + ], + "allOf": [ + { + "$ref": "#/definitions/CumulusCLIConfig" + } + ] } }, "additionalProperties": false, "definitions": { + "StepUIOptions": { + "title": "Task Definition", + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "is_required": { + "title": "Is Required", + "type": "boolean" + }, + "is_recommended": { + "title": "Is Recommended", + "type": "boolean" + }, + "is_optional": { + "title": "Is Optional", + "type": "boolean" + } + }, + "required": ["name", "is_required", "is_recommended", "is_optional"], + "additionalProperties": false + }, "Task": { - "title": "Task", + "title": "Task Definition", "type": "object", "properties": { "class_path": { "title": "Class Path", + "description": "The Python class path to the task class. CumulusCI includes the repo root in the Python path, so you can use a relative path from the repo root.", + "examples": [ + "cumulusci.tasks.salesforce.Deploy", + "tasks.my_task.MyTask" + ], "type": "string" }, "description": { "title": "Description", + "description": "A description of the task shown via the cli in task list, task info, and when running the task directly or via a flow.", + "examples": ["Deploys the post-install metadata."], "type": "string" }, "group": { "title": "Group", + "description": "The group of the task, used to group tasks in cci task list.", + "examples": ["Sample Data", "My Custom Group"], "type": "string" }, "options": { - "title": "Options", + "title": "Task Options", + "description": "Options for the task. Tasks in CumulusCI can accept specific options. Use cci task info to list available options for a particular task.", "default": {}, "additionalProperties": true, + "examples": [ + { + "path": "src" + } + ], "type": "object" }, "ui_options": { - "title": "Ui Options", + "title": "UI Options", + "description": "Options for the task or flow that are only used by MetaDeploy. For flows, this should be a dictionary with a key for the task name containing keys for each option.", "default": {}, "additionalProperties": true, - "type": "object" + "examples": [ + { + "name": "Deploy Recommended Config", + "is_recommended": true + }, + { + "name": "Deploy Optional Config", + "is_optional": true + } + ], + "allOf": [ + { + "$ref": "#/definitions/StepUIOptions" + } + ] }, "name": { "title": "Name", @@ -115,61 +490,126 @@ }, "additionalProperties": false }, + "PreflightCheckActionEnum": { + "title": "PreflightCheckActionEnum", + "description": "The allowed values for the action field of a preflight check", + "enum": ["error", "warn", "optional", "skip"], + "type": "string" + }, "PreflightCheck": { - "title": "PreflightCheck", + "title": "Preflight Check Definition", "type": "object", "properties": { "when": { - "title": "When", + "title": "When Clause", + "description": "A Jinja expression used to determine if the preflight check triggers.", + "examples": [ + "org.scratch == True", + "'npsp' not in tasks.get_installed_packages()" + ], "type": "string" }, "action": { "title": "Action", - "type": "string" + "description": "The action to take if the preflight check when clause evaluates to True.", + "examples": ["error", "warn", "optional", "skip"], + "allOf": [ + { + "$ref": "#/definitions/PreflightCheckActionEnum" + } + ] }, "message": { "title": "Message", + "description": "The message to display if the preflight check evaluates to True.", + "examples": ["The org must be a scratch org."], "type": "string" } }, "additionalProperties": false }, "Step": { - "title": "Step", + "title": "Step Definition", "type": "object", "properties": { "task": { "title": "Task", + "description": "The name of the task to run", + "examples": ["deploy", "load_custom_settings"], "type": "string" }, "flow": { "title": "Flow", + "description": "The name of the flow to run", + "examples": ["dependencies", "config_dev"], "type": "string" }, "ignore_failure": { "title": "Ignore Failure", + "description": "If True, ignore failures on this step and continues with the next step.", "default": false, "type": "boolean" }, "when": { "title": "When", + "description": "A Jinja expression to determine if the step should run", + "examples": [ + "org.scratch == True", + "'npsp' not in tasks.get_installed_packages()" + ], "type": "string" }, "options": { "title": "Options", + "description": "Options for the task or flow. For flows, this should be a dictionary with a key for the task name containing keys for each option.", "default": {}, "additionalProperties": true, + "examples": [ + { + "path": "src" + }, + { + "deploy": { + "path": "src" + } + } + ], "type": "object" }, "ui_options": { - "title": "Ui Options", + "title": "UI Options", + "description": "Options for the task or flow that are only used by MetaDeploy. For flows, this should be a dictionary with a key for the task name containing keys for each option.", "default": {}, "additionalProperties": true, - "type": "object" + "examples": [ + { + "name": "Deploy Recommended Config", + "is_recommended": true + }, + { + "name": "Deploy Optional Config", + "is_optional": true + } + ], + "allOf": [ + { + "$ref": "#/definitions/StepUIOptions" + } + ] }, "checks": { - "title": "Checks", + "title": "Preflight Checks", + "description": "A list of preflight checks to run before the step.", "default": [], + "examples": [ + [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org." + } + ] + ], "type": "array", "items": { "$ref": "#/definitions/PreflightCheck" @@ -177,21 +617,38 @@ }, "description": { "title": "Description", + "description": "A description of the step, shown in output logs when running the flow.", "type": "string" } }, "additionalProperties": false }, "Flow": { - "title": "Flow", + "title": "Flow Definition", "type": "object", "properties": { "description": { "title": "Description", + "description": "A description of the flow shown via the cli in flow list, flow info, and when running the flow.", + "examples": ["Sets up a new dev org."], "type": "string" }, "steps": { "title": "Steps", + "description": "The steps in the flow. Steps are entered in numbered slots using the key. Slot numbers are parsed with Python's LooseVersion to allow for injecting steps between other steps using the format 1.1.2.", + "examples": [ + { + "1": { + "task": "deploy" + }, + "1.1": { + "task": "my-custom-task" + }, + "2": { + "task": "run_tests" + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/Step" @@ -199,52 +656,70 @@ }, "group": { "title": "Group", + "description": "The group of the flow. Used to group flows in cci flow list.", + "examples": ["Org Setup", "My Custom Group"], "type": "string" } }, "additionalProperties": false }, "Package": { - "title": "Package", + "title": "Package Configuration", "type": "object", "properties": { "name": { - "title": "Name", + "title": "Package Name", + "description": "The name of the package in the packaging org or DevHub.", + "examples": ["Nonprofit Success Pack", "My Custom Package"], "type": "string" }, "name_managed": { - "title": "Name Managed", + "title": "Managed Package Name Override", + "description": "If set, this will override the package name for deployments to the 1GP managed packages. Use if you need to have a different package name in the 1GP packaging org than in all other orgs.", + "examples": ["My Custom 1GP Package Name"], "type": "string" }, "namespace": { - "title": "Namespace", + "title": "Package Namespace", + "description": "The Salesforce assigned package namespace for the package.", + "examples": ["npsp", "mynamespace"], "type": "string" }, "install_class": { "title": "Install Class", + "description": "The name of the Apex class to run during package install. This value is used when creating managed package versions.", + "examples": ["npsp.InstallScript"], "type": "string" }, "uninstall_class": { "title": "Uninstall Class", + "description": "The name of the Apex class to run during package uninstall. This value is used when creating managed package versions.", + "examples": ["npsp.UninstallScript"], "type": "string" }, "api_version": { - "title": "Api Version", + "title": "API Version", + "description": "The API version to use when deploying and retrieving the project.", + "examples": ["50.0"], "type": "string" }, "metadata_package_id": { - "title": "Metadata Package Id", + "title": "Metadata Package ID", + "description": "The Metadata Package ID for 2GP packages.", + "examples": ["033000000000000AAA"], "type": "string" } }, "additionalProperties": false }, "Test": { - "title": "Test", + "title": "Apex Test Configuration", "type": "object", "properties": { "name_match": { - "title": "Name Match", + "title": "Apex Test Name Match", + "description": "A SOQL LIKE expression to match Apex test class names to run.", + "examples": ["%Test", "%_test%"], "type": "string" } }, @@ -252,15 +727,19 @@ "additionalProperties": false }, "ReleaseNotesParser": { - "title": "ReleaseNotesParser", + "title": "Release Notes Parsers Configuration", "type": "object", "properties": { "class_path": { "title": "Class Path", + "description": "The Python path to the release notes parser class.", + "examples": ["cumulusci.core.release_notes.parsers.BaseParser"], "type": "string" }, "title": { "title": "Title", + "description": "The title of the release notes parser.", + "examples": ["Base Release Notes Parser"], "type": "string" } }, @@ -268,80 +747,179 @@ "additionalProperties": false }, "ReleaseNotes": { - "title": "ReleaseNotes", + "title": "Release Notes Configuration", "type": "object", "properties": { "parsers": { "title": "Parsers", + "description": "A dictionary of release notes parsers in execution order.", + "default": {}, + "examples": [ + { + "1": { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Critical Changes" + }, + "2": { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Changes" + }, + "3": { + "class_path": "cumulusci.tasks.release_notes.parser.GithubIssuesParser", + "title": "Issues Closed" + }, + "4": { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "New Metadata" + }, + "5": { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Deleted Metadata" + }, + "6": { + "class_path": "cumulusci.tasks.release_notes.parser.InstallLinkParser", + "title": "Installation Info" + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/ReleaseNotesParser" } } }, - "required": ["parsers"], "additionalProperties": false }, "Git": { - "title": "Git", + "title": "Git Configuration", "type": "object", "properties": { "repo_url": { - "title": "Repo Url", + "title": "Repo URL", + "description": "The URL of the repo.", + "examples": ["https://github.com/SalesforceFoundation/NPSP"], "type": "string" }, "default_branch": { "title": "Default Branch", + "description": "The default branch of the repo.", + "examples": ["main"], "type": "string" }, "prefix_feature": { - "title": "Prefix Feature", + "title": "Feature Branch Prefix", + "description": "The prefix for feature branches.", + "examples": ["feature/"], "type": "string" }, "prefix_beta": { - "title": "Prefix Beta", + "title": "Beta Branch Prefix", + "description": "The prefix for beta branches.", + "examples": ["beta/"], "type": "string" }, "prefix_release": { - "title": "Prefix Release", + "title": "Release Branch Prefix", + "description": "The prefix for release branches.", + "examples": ["release/"], "type": "string" }, "push_prefix_sandbox": { - "title": "Push Prefix Sandbox", + "title": "Sandbox Push Prefix", + "description": "The prefix for sandbox push branches.", + "examples": ["sandbox/"], "type": "string" }, "push_prefix_production": { - "title": "Push Prefix Production", + "title": "Production Push Prefix", + "description": "The prefix for production push branches.", + "examples": ["prod/"], "type": "string" }, "release_notes": { - "$ref": "#/definitions/ReleaseNotes" + "title": "Release Notes", + "description": "The release notes configuration.", + "examples": [ + { + "parsers": { + "1": { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Critical Changes" + }, + "2": { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Changes" + } + } + } + ], + "allOf": [ + { + "$ref": "#/definitions/ReleaseNotes" + } + ] }, "2gp_context": { - "title": "2Gp Context", + "title": "2GP Context", + "description": "The GitHub Commit Status name to use for 2GP feature packages.", + "examples": [ + "Build Feature Test Package", + "Nonstandard Package Status" + ], "type": "string" }, "unlocked_context": { "title": "Unlocked Context", + "description": "The GitHub Commit Status name to use for unlocked packages.", + "examples": ["Build Unlocked Package", "Nonstandard Package Status"], "type": "string" } }, "additionalProperties": false }, + "ResolutionStrategiesEnum": { + "title": "ResolutionStrategiesEnum", + "description": "The allowed values for the resolution_strategy field of a dependency resolution", + "enum": ["latest_release", "include_beta", "commit_status", "unlocked"], + "type": "string" + }, "DependencyResolutions": { - "title": "DependencyResolutions", + "title": "Dependency Resolutions", "type": "object", "properties": { "production": { - "title": "Production", - "type": "string" + "title": "Production Resolution", + "description": "The resolution strategy to use for production orgs including packaging orgs and promotable package versions.", + "examples": ["latest_release"], + "anyOf": [ + { + "$ref": "#/definitions/ResolutionStrategiesEnum" + }, + { + "type": "string" + } + ] }, "preproduction": { - "title": "Preproduction", + "title": "Preproduction Resolution", + "description": "The resolution strategy to use for preproduction orgs including scratch orgs.", + "examples": ["include_beta", "commit_status", "my_custom_strategy"], "type": "string" }, "resolution_strategies": { "title": "Resolution Strategies", + "description": "A dictionary of resolution strategies made up of a list of resolvers run in order until resolution is found.", + "examples": [ + { + "latest_release": ["tag", "latest_release", "unmanaged"], + "include_beta": [ + "tag", + "latest_beta", + "latest_release", + "unmanaged" + ] + } + ], "type": "object", "additionalProperties": { "type": "array", @@ -354,24 +932,82 @@ "additionalProperties": false }, "Project": { - "title": "Project", + "title": "Project Configuration", "type": "object", "properties": { "name": { - "title": "Name", + "title": "Project Name", + "description": "The name of the project. This should generally match the git repo name. This name is used as the project name in CumulusCI's keychain for storing files under ~/.cumulusci.", + "examples": ["NPSP", "My-Repo"], "type": "string" }, "package": { - "$ref": "#/definitions/Package" + "title": "Package Config", + "description": "The package info for the project.", + "examples": [ + { + "name": "Nonprofit Success Pack", + "namespace": "npsp", + "api_version": "59.0" + } + ], + "allOf": [ + { + "$ref": "#/definitions/Package" + } + ] }, "test": { - "$ref": "#/definitions/Test" + "title": "Apex Testing Config", + "description": "The apex test configuration for the project.", + "examples": [ + { + "name_match": "%Test" + }, + { + "name_match": "%_test%" + } + ], + "allOf": [ + { + "$ref": "#/definitions/Test" + } + ] }, "git": { - "$ref": "#/definitions/Git" + "title": "Git Config", + "description": "The git configuration for the project.", + "examples": [ + { + "repo_url": "https://github.com/SalesforceFoundation/NPSP", + "default_branch": "main", + "prefix_feature": "feature/", + "prefix_beta": "beta/", + "prefix_release": "release/", + "push_prefix_sandbox": "sandbox/", + "push_prefix_production": "prod/" + } + ], + "allOf": [ + { + "$ref": "#/definitions/Git" + } + ] }, "dependencies": { "title": "Dependencies", + "description": "A list of dependencies for the project. See https://cumulusci.readthedocs.io/en/stable/dev.html#manage-dependencies for more information.", + "examples": [ + [ + { + "namespace": "somens", + "version": "1.23" + }, + { + "github": "https://github.com/SalesforceFoundation/NPSP" + } + ] + ], "type": "array", "items": { "type": "object", @@ -381,10 +1017,44 @@ } }, "dependency_resolutions": { - "$ref": "#/definitions/DependencyResolutions" + "title": "Dependency Resolutions", + "description": "The dependency resolution configuration for the project. See https://cumulusci.readthedocs.io/en/stable/dev.html#manage-dependencies for more information.", + "examples": [ + { + "production": "latest_release", + "preproduction": "include_beta", + "resolution_strategies": { + "latest_release": ["tag", "latest_release", "unmanaged"], + "include_beta": [ + "tag", + "latest_beta", + "latest_release", + "unmanaged" + ] + } + } + ], + "allOf": [ + { + "$ref": "#/definitions/DependencyResolutions" + } + ] }, "dependency_pins": { "title": "Dependency Pins", + "description": "A list of dependency pins for the project. See https://cumulusci.readthedocs.io/en/stable/dev.html#manage-dependencies for more information.", + "examples": [ + [ + { + "github": "https://github.com/SalesforceFoundation/NPSP", + "tag": "rel/3.219" + }, + { + "github": "https://github.com/SalesforceFoundation/Contacts_and_Organizations", + "tag": "rel/3.19" + } + ] + ], "type": "array", "items": { "type": "object", @@ -395,44 +1065,70 @@ }, "source_format": { "title": "Source Format", + "description": "The source format for the project. Either sfdx for sfdx source format under force-app/ or mdapi for Metadata API format under src/.", "default": "mdapi", + "examples": ["sfdx", "mdapi"], "enum": ["sfdx", "mdapi"], "type": "string" }, "custom": { "title": "Custom", + "description": "A dictionary for custom project configuration. This is a good place to put project-specific configuration. Do not use for storing any sensitive information like credentials!", + "examples": [ + { + "my_custom_key": "my_custom_value" + } + ], "type": "object" } }, "additionalProperties": false }, "ScratchOrg": { - "title": "ScratchOrg", + "title": "Scratch Org Configuration", "type": "object", "properties": { "config_file": { "title": "Config File", + "description": "The path to the scratchdef config file.", + "examples": [ + "orgs/dev.json", + "orgs/managed.json", + "config/project-scratch-def.json" + ], "type": "string", "format": "path" }, "days": { "title": "Days", + "description": "The number of days the scratch org should last.", + "min": 1, + "max": 30, + "examples": [1, 7, 30], "type": "integer" }, "namespaced": { "title": "Namespaced", - "type": "string" + "description": "If true, the scratch org should be created with the project's namespace. If false, the scratch org should be created without a namespace.", + "examples": [true, false], + "type": "boolean" }, "setup_flow": { "title": "Setup Flow", + "description": "The name of the flow to run after the scratch org is created.", + "examples": ["dev_org", "qa_org_2gp"], "type": "string" }, "noancestors": { - "title": "Noancestors", + "title": "No Ancestors", + "description": "If True, the scratch org should be created without ancestors.", + "examples": [true, false], "type": "boolean" }, "release": { "title": "Release", + "description": "The Salesforce release to use for the scratch org.", + "examples": ["preview", "previous"], "enum": ["preview", "previous"], "type": "string" } @@ -440,11 +1136,32 @@ "additionalProperties": false }, "Orgs": { - "title": "Orgs", + "title": "Orgs Configuration", "type": "object", "properties": { "scratch": { - "title": "Scratch", + "title": "Scratch Org Configs", + "description": "A dictionary of scratch org configurations for the project.", + "examples": [ + { + "dev": { + "config_file": "orgs/dev.json", + "days": 1, + "namespaced": true, + "setup_flow": "dev_org", + "noancestors": true, + "release": "preview" + }, + "qa": { + "config_file": "orgs/qa.json", + "days": 7, + "namespaced": true, + "setup_flow": "qa_org", + "noancestors": true, + "release": "previous" + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/ScratchOrg" @@ -454,47 +1171,92 @@ "additionalProperties": false }, "ServiceAttribute": { - "title": "ServiceAttribute", + "title": "Service Attribute Definition", "type": "object", "properties": { "description": { "title": "Description", + "description": "A description of the attribute.", + "examples": ["The username for the service."], "type": "string" }, "required": { "title": "Required", + "description": "If True, the attribute is required.", + "examples": [true, false], "type": "boolean" }, "default_factory": { "title": "Default Factory", + "description": "The Python class path to a callable that returns the default value for the attribute.", + "examples": ["cumulusci.core.config.OrgConfig.default_username"], "type": "string" }, "default": { "title": "Default", + "description": "The default value for the attribute.", + "examples": ["my_default_username"], "type": "string" }, "sensitive": { "title": "Sensitive", + "description": "If True, the attribute is sensitive and should not be logged.", "default": false, + "examples": [true, false], "type": "boolean" } }, "additionalProperties": false }, "Service": { - "title": "Service", + "title": "Service Definition", "type": "object", "properties": { "description": { "title": "Description", + "description": "A description of the service.", + "examples": [ + "Create a connection to GitHub to interact with the API." + ], "type": "string" }, "class_path": { "title": "Class Path", + "description": "The Python class path to the service class. Typically not used for most services that just need simple key/value configuration.", + "examples": [ + "cumulusci.core.config.marketing_cloud_service_config.MarketingCloudServiceConfig" + ], "type": "string" }, "attributes": { "title": "Attributes", + "description": "A dictionary of attributes for the service.", + "examples": [ + { + "username": { + "description": "The username for the service.", + "required": true, + "default_factory": "cumulusci.core.config.OrgConfig.default_username" + }, + "password": { + "description": "The password for the service.", + "required": true, + "sensitive": true + } + }, + { + "base_url": { + "description": "The base URL for the service.", + "required": true, + "default": "https://my-service.com" + }, + "api_token": { + "description": "The API token for the service.", + "required": true, + "sensitive": true + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/ServiceAttribute" @@ -502,17 +1264,24 @@ }, "validator": { "title": "Validator", + "description": "The Python class path to a callable that validates the service configuration.", + "examples": ["cumulusci.core.github.validate_service"], "type": "string" } }, "additionalProperties": false }, "CumulusCIConfig": { - "title": "CumulusCIConfig", + "title": "CumulusCI Configuration", "type": "object", "properties": { "keychain": { - "title": "Keychain", + "title": "Keychain Class Path", + "description": "The Python class path to the keychain class to override CumulusCI's keychain.", + "examples": [ + "cumulusci.core.keychain.EnvironmentProjectKeychain", + "mypypackage.keychain.MyCumulusCIKeychain" + ], "type": "string" } }, @@ -520,42 +1289,107 @@ "additionalProperties": false }, "Plan": { - "title": "Plan", + "title": "Plan Definition", "type": "object", "properties": { "title": { - "title": "Title", + "title": "Plan Title", + "description": "The title of the plan, shown in MetaDeploy.", + "examples": ["Base Install", "Full Demo Install"], "type": "string" }, "description": { "title": "Description", + "description": "A description of the plan.", + "examples": [ + "A base install of the packages only", + "A full demo install of the packages with configuration and sample data" + ], "type": "string" }, "tier": { "title": "Tier", + "description": "The tier of the plan. Primary plans are shown first in MetaDeploy. Secondary plans are shown second. Additional plans are shown last. Only one primary plan and one secondary plan can be set per project. You can set multiple additional plans.", "default": "primary", + "examples": ["primary", "secondary", "additional"], "enum": ["primary", "secondary", "additional"], "type": "string" }, "slug": { "title": "Slug", + "description": "The slug of the plan, used in the plan URL.", + "examples": ["base-install", "full-demo-install"], "type": "string" }, "is_listed": { "title": "Is Listed", + "description": "If True, the plan is listed in MetaDeploy. If False, the plan is hidden.", "default": true, "type": "boolean" }, "steps": { "title": "Steps", + "description": "The steps in the plan. Steps are entered in numbered slots using the key. Slot numbers are parsed with Python's LooseVersion to allow for injecting steps between other steps using the format 1.1.2.", + "examples": [ + { + "1": { + "task": "update_dependencies" + }, + "2": { + "task": "install_prod" + } + }, + { + "1": { + "task": "update_dependencies" + }, + "2": { + "task": "install_prod" + }, + "3": { + "flow": "config_common" + }, + "4": { + "flow": "config_demo" + }, + "5": { + "flow": "demo_data" + } + }, + { + "1": { + "flow": "customer_org" + } + }, + { + "1": { + "task": "customer_org_full" + } + } + ], "type": "object", "additionalProperties": { "$ref": "#/definitions/Step" } }, "checks": { - "title": "Checks", + "title": "Preflight Checks", + "description": "A list of preflight checks to run before the plan.", "default": [], + "examples": [ + [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org." + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org." + } + ] + ], "type": "array", "items": { "$ref": "#/definitions/PreflightCheck" @@ -563,19 +1397,29 @@ }, "error_message": { "title": "Error Message", + "description": "The error message to display if the preflight check is triggered.", + "examples": ["The org must be a scratch org."], "type": "string" }, "post_install_message": { - "title": "Post Install Message", + "title": "Post-install Message", + "description": "The message to display after the plan is installed.", + "examples": ["The plan has been installed."], "type": "string" }, "preflight_message": { "title": "Preflight Message", + "description": "The message to display before the plan is installed.", + "examples": [ + "The plan is about to be installed. We're running some checks on your org first." + ], "type": "string" }, "allowed_org_providers": { "title": "Allowed Org Providers", + "description": "The org providers that are allowed for the plan. The user provider allows connections to existing orgs using OAuth. The devhub provider uses MetaDeploy's configured DevHub to create scratch orgs.", "default": ["user"], + "examples": [["devhub"], ["user"], ["devhub", "user"]], "type": "array", "items": { "enum": ["devhub", "user"], @@ -586,17 +1430,21 @@ "additionalProperties": false }, "LocalFolderSourceModel": { - "title": "LocalFolderSourceModel", + "title": "Local Folder Source Definition", "type": "object", "properties": { "path": { - "title": "Path", + "title": "Folder Path", + "description": "The path to the folder.", + "examples": ["modules"], "format": "directory-path", "type": "string" }, "allow_remote_code": { "title": "Allow Remote Code", + "description": "If True, allow remote code execution for the source. This is a security risk and should only be used for trusted sources.", "default": false, + "examples": [true, false], "type": "boolean" } }, @@ -610,43 +1458,71 @@ "type": "string" }, "GitHubSourceModel": { - "title": "GitHubSourceModel", + "title": "GitHub Source Definition", "type": "object", "properties": { "github": { - "title": "Github", + "title": "GitHub URL", + "description": "The GitHub URL for the repository.", + "examples": ["https://github.com/SalesforceFoundation/NPSP"], "type": "string" }, "resolution_strategy": { "title": "Resolution Strategy", + "description": "The resolution strategy to use for the GitHub source.", + "examples": [ + "latest_release", + "include_beta", + "commit_status", + "unlocked" + ], "type": "string" }, "commit": { "title": "Commit", + "description": "The commit to use for the GitHub source.", + "examples": ["abc123"], "type": "string" }, "ref": { "title": "Ref", + "description": "The ref to use for the GitHub source.", + "examples": ["main"], "type": "string" }, "branch": { "title": "Branch", + "description": "The branch to use for the GitHub source.", + "examples": ["main"], "type": "string" }, "tag": { "title": "Tag", + "description": "The tag to use for the GitHub source.", + "examples": ["v1.0"], "type": "string" }, "release": { - "$ref": "#/definitions/GitHubSourceRelease" + "title": "Release", + "description": "The release to use for the GitHub source.", + "examples": ["latest", "previous", "latest_beta"], + "allOf": [ + { + "$ref": "#/definitions/GitHubSourceRelease" + } + ] }, "description": { "title": "Description", + "description": "A description of the source.", + "examples": ["The GitHub source for the Nonprofit Success Pack."], "type": "string" }, "allow_remote_code": { "title": "Allow Remote Code", + "description": "If True, allow remote code execution for the source. This is a security risk and should only be used for trusted sources.", "default": false, + "examples": [true, false], "type": "boolean" } }, @@ -654,16 +1530,20 @@ "additionalProperties": false }, "CumulusCLIConfig": { - "title": "CumulusCLIConfig", + "title": "CumulusCI CLI Configuration", "type": "object", "properties": { "show_stacktraces": { "title": "Show Stacktraces", + "description": "If True, show stacktraces for exceptions.", "default": false, + "examples": [true, false], "type": "boolean" }, "plain_output": { "title": "Plain Output", + "description": "If True, use plain output instead of rich output.", + "examples": [true, false], "type": "boolean" } }, diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index f8498ed9ea..f0bd6256bc 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -33,21 +33,117 @@ VSCodeFriendlyDict = Field({}, additionalProperties=True) +class PreflightCheckActionEnum(StrEnum): + "The allowed values for the action field of a preflight check" + error = "error" + warn = "warn" + optional = "optional" + skip = "skip" + + class PreflightCheck(CCIDictModel): - when: str = None - action: str = None - message: str = None + when: str = Field( + None, + title="When Clause", + description="A Jinja expression used to determine if the preflight check triggers.", + examples=[ + "org.scratch == True", + "'npsp' not in tasks.get_installed_packages()", + ], + ) + action: PreflightCheckActionEnum = Field( + None, + title="Action", + description="The action to take if the preflight check when clause evaluates to True.", + examples=["error", "warn", "optional", "skip"], + ) + message: Optional[str] = Field( + None, + title="Message", + description="The message to display if the preflight check evaluates to True.", + examples=["The org must be a scratch org."], + ) + + class Config: + title = "Preflight Check Definition" + description = "A preflight check to run before a step or plan in MetaDeploy." + + +class StepUIOptions(TypedDict): + name: str # Name of the step shown to the user. + is_required: bool # If True, the step will always be run. + is_recommended: bool # If True, the step will be run unless the user opts out. + is_optional: bool # If True, the step will be run only if the user opts in. class Step(CCIDictModel): - task: str = None - flow: str = None - ignore_failure: bool = False - when: str = None # is this allowed? - options: Dict[str, Any] = VSCodeFriendlyDict - ui_options: Dict[str, Any] = VSCodeFriendlyDict - checks: List[PreflightCheck] = [] - description: str = None + task: str = Field( + None, + title="Task", + description="The name of the task to run", + examples=["deploy", "load_custom_settings"], + ) + flow: str = Field( + None, + title="Flow", + description="The name of the flow to run", + examples=["dependencies", "config_dev"], + ) + ignore_failure: bool = Field( + False, + title="Ignore Failure", + description="If True, ignore failures on this step and continues with the next step.", + ) + # is this allowed? + when: str = Field( + None, + title="When", + description="A Jinja expression to determine if the step should run", + examples=[ + "org.scratch == True", + "'npsp' not in tasks.get_installed_packages()", + ], + ) + options: Dict[str, Any] = Field( + {}, + additionalProperties=True, + title="Options", + description="Options for the task or flow. For flows, this should be a dictionary with a key for the task name containing keys for each option.", + examples=[{"path": "src"}, {"deploy": {"path": "src"}}], + ) + ui_options: StepUIOptions = Field( + {}, + additionalProperties=True, + title="UI Options", + description="Options for the task or flow that are only used by MetaDeploy. For flows, this should be a dictionary with a key for the task name containing keys for each option.", + examples=[ + {"name": "Deploy Recommended Config", "is_recommended": True}, + {"name": "Deploy Optional Config", "is_optional": True}, + ], + ) + checks: List[PreflightCheck] = Field( + [], + title="Preflight Checks", + description="A list of preflight checks to run before the step.", + examples=[ + [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org.", + } + ] + ], + ) + description: str = Field( + None, + title="Description", + description="A description of the step, shown in output logs when running the flow.", + ) + + class Config: + title = "Step Definition" + description = "A step to run in a flow. Steps are either a task or a flow and can contain configuration options for the task or flow." @root_validator() def _check(cls, values): @@ -60,120 +156,731 @@ def _check(cls, values): class Task(CCIDictModel): - class_path: str = None - description: str = None - group: str = None + class_path: str = Field( + None, + title="Class Path", + description="The Python class path to the task class. CumulusCI includes the repo root in the Python path, so you can use a relative path from the repo root.", + examples=["cumulusci.tasks.salesforce.Deploy", "tasks.my_task.MyTask"], + ) + description: str = Field( + None, + title="Description", + description="A description of the task shown via the cli in task list, task info, and when running the task directly or via a flow.", + examples=["Deploys the post-install metadata."], + ) + group: str = Field( + None, + title="Group", + description="The group of the task, used to group tasks in cci task list.", + examples=["Sample Data", "My Custom Group"], + ) # additionalProperties here works around an # incompatibility with VSCode's Red Hat YAML validator - options: Dict[str, Any] = VSCodeFriendlyDict - ui_options: Dict[str, Any] = VSCodeFriendlyDict - name: str = None # get rid of this??? + options: Dict[str, Any] = Field( + {}, + additionalProperties=True, + title="Task Options", + description="Options for the task. Tasks in CumulusCI can accept specific options. Use cci task info to list available options for a particular task.", + examples=[{"path": "src"}], + ) + ui_options: StepUIOptions = Field( + {}, + additionalProperties=True, + title="UI Options", + description="Options for the task or flow that are only used by MetaDeploy. For flows, this should be a dictionary with a key for the task name containing keys for each option.", + examples=[ + {"name": "Deploy Recommended Config", "is_recommended": True}, + {"name": "Deploy Optional Config", "is_optional": True}, + ], + ) + name: Optional[str] = None # get rid of this??? + + class Config: + title = "Task Definition" + description = "A task to run in a flow. Tasks are Python classes that perform a specific action." class Flow(CCIDictModel): - description: str = None - steps: Dict[str, Step] = None - group: str = None + description: str = Field( + None, + title="Description", + description="A description of the flow shown via the cli in flow list, flow info, and when running the flow.", + examples=["Sets up a new dev org."], + ) + steps: Dict[str, Step] = Field( + None, + title="Steps", + description="The steps in the flow. Steps are entered in numbered slots using the key. Slot numbers are parsed with Python's LooseVersion to allow for injecting steps between other steps using the format 1.1.2.", + examples=[ + { + "1": {"task": "deploy"}, + "1.1": {"task": "my-custom-task"}, + "2": {"task": "run_tests"}, + } + ], + ) + group: str = Field( + None, + title="Group", + description="The group of the flow. Used to group flows in cci flow list.", + examples=["Org Setup", "My Custom Group"], + ) + + class Config: + title = "Flow Definition" + description = ( + "A flow to run. Flows are a series of steps that can be run in order." + ) class Package(CCIDictModel): - name: Optional[str] = None - name_managed: Optional[str] = None - namespace: Optional[str] = None - install_class: str = None - uninstall_class: str = None - api_version: str = None - metadata_package_id: str = None + name: Optional[str] = Field( + None, + title="Package Name", + description="The name of the package in the packaging org or DevHub.", + examples=["Nonprofit Success Pack", "My Custom Package"], + ) + name_managed: Optional[str] = Field( + None, + title="Managed Package Name Override", + description="If set, this will override the package name for deployments to the 1GP managed packages. Use if you need to have a different package name in the 1GP packaging org than in all other orgs.", + examples=["My Custom 1GP Package Name"], + ) + namespace: Optional[str] = Field( + None, + title="Package Namespace", + description="The Salesforce assigned package namespace for the package.", + examples=["npsp", "mynamespace"], + ) + install_class: str = Field( + None, + title="Install Class", + description="The name of the Apex class to run during package install. This value is used when creating managed package versions.", + examples=["npsp.InstallScript"], + ) + uninstall_class: str = Field( + None, + title="Uninstall Class", + description="The name of the Apex class to run during package uninstall. This value is used when creating managed package versions.", + examples=["npsp.UninstallScript"], + ) + api_version: str = Field( + None, + title="API Version", + description="The API version to use when deploying and retrieving the project.", + examples=["50.0"], + ) + metadata_package_id: str = Field( + None, + title="Metadata Package ID", + description="The Metadata Package ID for 2GP packages.", + examples=["033000000000000AAA"], + ) + + class Config: + title = "Package Configuration" + description = "The package configuration for the project." class Test(CCIDictModel): - name_match: str + name_match: str = Field( + ..., + title="Apex Test Name Match", + description="A SOQL LIKE expression to match Apex test class names to run.", + examples=["%Test", "%_test%"], + ) + + class Config: + title = "Apex Test Configuration" + description = "The apex test configuration for the project." class ReleaseNotesParser(CCIDictModel): - class_path: PythonClassPath - title: str + class_path: PythonClassPath = Field( + ..., + title="Class Path", + description="The Python path to the release notes parser class.", + examples=["cumulusci.core.release_notes.parsers.BaseParser"], + ) + title: str = Field( + ..., + title="Title", + description="The title of the release notes parser.", + examples=["Base Release Notes Parser"], + ) + + class Config: + title = "Release Notes Parsers Configuration" + description = ( + "A release notes parser to use for generating automated release notes." + ) class ReleaseNotes(CCIDictModel): - parsers: Dict[int, ReleaseNotesParser] + parsers: Dict[int, ReleaseNotesParser] = Field( + {}, + title="Parsers", + description="A dictionary of release notes parsers in execution order.", + examples=[ + { + 1: { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Critical Changes", + }, + 2: { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Changes", + }, + 3: { + "class_path": "cumulusci.tasks.release_notes.parser.GithubIssuesParser", + "title": "Issues Closed", + }, + 4: { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "New Metadata", + }, + 5: { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Deleted Metadata", + }, + 6: { + "class_path": "cumulusci.tasks.release_notes.parser.InstallLinkParser", + "title": "Installation Info", + }, + } + ], + ) + + class Config: + title = "Release Notes Configuration" + description = "The release notes configuration for the project." class Git(CCIDictModel): - repo_url: str = None - default_branch: str = None - prefix_feature: str = None - prefix_beta: str = None - prefix_release: str = None - push_prefix_sandbox: str = None - push_prefix_production: str = None - release_notes: ReleaseNotes = None - two_gp_context: str = Field(None, alias="2gp_context") - unlocked_context: Optional[str] = None + repo_url: str = Field( + None, + title="Repo URL", + description="The URL of the repo.", + examples=["https://github.com/SalesforceFoundation/NPSP"], + ) + default_branch: str = Field( + None, + title="Default Branch", + description="The default branch of the repo.", + examples=["main"], + ) + prefix_feature: str = Field( + None, + title="Feature Branch Prefix", + description="The prefix for feature branches.", + examples=["feature/"], + ) + prefix_beta: str = Field( + None, + title="Beta Branch Prefix", + description="The prefix for beta branches.", + examples=["beta/"], + ) + prefix_release: str = Field( + None, + title="Release Branch Prefix", + description="The prefix for release branches.", + examples=["release/"], + ) + push_prefix_sandbox: str = Field( + None, + title="Sandbox Push Prefix", + description="The prefix for sandbox push branches.", + examples=["sandbox/"], + ) + push_prefix_production: str = Field( + None, + title="Production Push Prefix", + description="The prefix for production push branches.", + examples=["prod/"], + ) + release_notes: ReleaseNotes = Field( + None, + title="Release Notes", + description="The release notes configuration.", + examples=[ + { + "parsers": { + 1: { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Critical Changes", + }, + 2: { + "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", + "title": "Changes", + }, + } + } + ], + ) + two_gp_context: str = Field( + None, + alias="2gp_context", + title="2GP Context", + description="The GitHub Commit Status name to use for 2GP feature packages.", + examples=["Build Feature Test Package", "Nonstandard Package Status"], + ) + unlocked_context: Optional[str] = Field( + None, + title="Unlocked Context", + description="The GitHub Commit Status name to use for unlocked packages.", + examples=["Build Unlocked Package", "Nonstandard Package Status"], + ) + + class Config: + title = "Git Configuration" + description = "The git configuration for the project." class Plan(CCIDictModel): # MetaDeploy plans - title: str = None - description: str = None - tier: Literal["primary", "secondary", "additional"] = "primary" - slug: str = None - is_listed: bool = True - steps: Dict[str, Step] = None - checks: List[PreflightCheck] = [] - error_message: str = None - post_install_message: str = None - preflight_message: str = None - allowed_org_providers: List[Literal["devhub", "user"]] = ["user"] + title: str = Field( + None, + title="Plan Title", + description="The title of the plan, shown in MetaDeploy.", + examples=["Base Install", "Full Demo Install"], + ) + description: str = Field( + None, + title="Description", + description="A description of the plan.", + examples=[ + "A base install of the packages only", + "A full demo install of the packages with configuration and sample data", + ], + ) + tier: Literal["primary", "secondary", "additional"] = Field( + "primary", + title="Tier", + description="The tier of the plan. Primary plans are shown first in MetaDeploy. Secondary plans are shown second. Additional plans are shown last. Only one primary plan and one secondary plan can be set per project. You can set multiple additional plans.", + examples=["primary", "secondary", "additional"], + ) + slug: str = Field( + None, + title="Slug", + description="The slug of the plan, used in the plan URL.", + examples=["base-install", "full-demo-install"], + ) + is_listed: bool = Field( + True, + title="Is Listed", + description="If True, the plan is listed in MetaDeploy. If False, the plan is hidden.", + ) + steps: Dict[str, Step] = Field( + None, + title="Steps", + description="The steps in the plan. Steps are entered in numbered slots using the key. Slot numbers are parsed with Python's LooseVersion to allow for injecting steps between other steps using the format 1.1.2.", + examples=[ + { + "1": {"task": "update_dependencies"}, + "2": {"task": "install_prod"}, + }, + { + "1": {"task": "update_dependencies"}, + "2": {"task": "install_prod"}, + "3": {"flow": "config_common"}, + "4": {"flow": "config_demo"}, + "5": {"flow": "demo_data"}, + }, + { + "1": {"flow": "customer_org"}, + }, + { + "1": {"task": "customer_org_full"}, + }, + ], + ) + checks: List[PreflightCheck] = Field( + [], + title="Preflight Checks", + description="A list of preflight checks to run before the plan.", + examples=[ + [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org.", + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org.", + }, + ] + ], + ) + error_message: str = Field( + None, + title="Error Message", + description="The error message to display if the preflight check is triggered.", + examples=["The org must be a scratch org."], + ) + post_install_message: str = Field( + None, + title="Post-install Message", + description="The message to display after the plan is installed.", + examples=["The plan has been installed."], + ) + preflight_message: str = Field( + None, + title="Preflight Message", + description="The message to display before the plan is installed.", + examples=[ + "The plan is about to be installed. We're running some checks on your org first." + ], + ) + allowed_org_providers: List[Literal["devhub", "user"]] = Field( + ["user"], + title="Allowed Org Providers", + description="The org providers that are allowed for the plan. The user provider allows connections to existing orgs using OAuth. The devhub provider uses MetaDeploy's configured DevHub to create scratch orgs.", + examples=[["devhub"], ["user"], ["devhub", "user"]], + ) + + class Config: + title = "Plan Definition" + description = "A plan to install in MetaDeploy. See https://metadeploy.readthedocs.io for more information." + + +class ResolutionStrategiesEnum(StrEnum): + """The allowed values for the resolution_strategy field of a dependency resolution""" + + latest_release = "latest_release" + include_beta = "include_beta" + commit_status = "commit_status" + unlocked = "unlocked" class DependencyResolutions(CCIDictModel): - production: str = None - preproduction: str = None - resolution_strategies: Dict[str, List[str]] = None + production: Union[ResolutionStrategiesEnum, str] = Field( + None, + title="Production Resolution", + description="The resolution strategy to use for production orgs including packaging orgs and promotable package versions.", + examples=["latest_release"], + ) + preproduction: str = Field( + None, + title="Preproduction Resolution", + description="The resolution strategy to use for preproduction orgs including scratch orgs.", + examples=["include_beta", "commit_status", "my_custom_strategy"], + ) + resolution_strategies: Dict[str, List[str]] = Field( + None, + title="Resolution Strategies", + description="A dictionary of resolution strategies made up of a list of resolvers run in order until resolution is found.", + examples=[ + { + "latest_release": ["tag", "latest_release", "unmanaged"], + "include_beta": ["tag", "latest_beta", "latest_release", "unmanaged"], + } + ], + ) + + class Config: + title = "Dependency Resolutions" + description = "The dependency resolution configuration for the project. See https://cumulusci.readthedocs.io/en/stable/dev.html#manage-dependencies for more information." class Project(CCIDictModel): - name: Optional[str] = None - package: Optional[Package] = None - test: Optional[Test] = None - git: Optional[Git] = None - dependencies: Optional[List[Dict[str, str]]] = None - dependency_resolutions: Optional[DependencyResolutions] = None - dependency_pins: Optional[List[Dict[str, str]]] - source_format: Literal["sfdx", "mdapi"] = "mdapi" - custom: Optional[Dict] = None + name: Optional[str] = Field( + None, + title="Project Name", + description="The name of the project. This should generally match the git repo name. This name is used as the project name in CumulusCI's keychain for storing files under ~/.cumulusci.", + examples=["NPSP", "My-Repo"], + ) + package: Optional[Package] = Field( + None, + title="Package Config", + description="The package info for the project.", + examples=[ + { + "name": "Nonprofit Success Pack", + "namespace": "npsp", + "api_version": "59.0", + } + ], + ) + test: Optional[Test] = Field( + None, + title="Apex Testing Config", + description="The apex test configuration for the project.", + examples=[{"name_match": "%Test"}, {"name_match": "%_test%"}], + ) + git: Optional[Git] = Field( + None, + title="Git Config", + description="The git configuration for the project.", + examples=[ + { + "repo_url": "https://github.com/SalesforceFoundation/NPSP", + "default_branch": "main", + "prefix_feature": "feature/", + "prefix_beta": "beta/", + "prefix_release": "release/", + "push_prefix_sandbox": "sandbox/", + "push_prefix_production": "prod/", + } + ], + ) + dependencies: Optional[List[Dict[str, str]]] = Field( + None, + title="Dependencies", + description="A list of dependencies for the project. See https://cumulusci.readthedocs.io/en/stable/dev.html#manage-dependencies for more information.", + examples=[ + [ + {"namespace": "somens", "version": "1.23"}, + {"github": "https://github.com/SalesforceFoundation/NPSP"}, + ], + ], + ) + dependency_resolutions: Optional[DependencyResolutions] = Field( + None, + title="Dependency Resolutions", + description="The dependency resolution configuration for the project. See https://cumulusci.readthedocs.io/en/stable/dev.html#manage-dependencies for more information.", + examples=[ + { + "production": "latest_release", + "preproduction": "include_beta", + "resolution_strategies": { + "latest_release": ["tag", "latest_release", "unmanaged"], + "include_beta": [ + "tag", + "latest_beta", + "latest_release", + "unmanaged", + ], + }, + } + ], + ) + dependency_pins: Optional[List[Dict[str, str]]] = Field( + None, + title="Dependency Pins", + description="A list of dependency pins for the project. See https://cumulusci.readthedocs.io/en/stable/dev.html#manage-dependencies for more information.", + examples=[ + [ + { + "github": "https://github.com/SalesforceFoundation/NPSP", + "tag": "rel/3.219", + }, + { + "github": "https://github.com/SalesforceFoundation/Contacts_and_Organizations", + "tag": "rel/3.19", + }, + ], + ], + ) + source_format: Literal["sfdx", "mdapi"] = Field( + "mdapi", + title="Source Format", + description="The source format for the project. Either sfdx for sfdx source format under force-app/ or mdapi for Metadata API format under src/.", + examples=["sfdx", "mdapi"], + ) + custom: Optional[Dict] = Field( + None, + title="Custom", + description="A dictionary for custom project configuration. This is a good place to put project-specific configuration. Do not use for storing any sensitive information like credentials!", + examples=[{"my_custom_key": "my_custom_value"}], + ) + + class Config: + title = "Project Configuration" + description = "The project configuration for the project. See https://cumulusci.readthedocs.io/en/stable/config.html for more information." class ScratchOrg(CCIDictModel): - config_file: Path = None - days: int = None - namespaced: str = None - setup_flow: str = None - noancestors: bool = None - release: Literal["preview", "previous"] = None + config_file: Path = Field( + None, + title="Config File", + description="The path to the scratchdef config file.", + examples=[ + "orgs/dev.json", + "orgs/managed.json", + "config/project-scratch-def.json", + ], + ) + days: int = Field( + None, + title="Days", + description="The number of days the scratch org should last.", + min=1, + max=30, + examples=[1, 7, 30], + ) + namespaced: bool = Field( + None, + title="Namespaced", + description="If true, the scratch org should be created with the project's namespace. If false, the scratch org should be created without a namespace.", + examples=[True, False], + ) + setup_flow: str = Field( + None, + title="Setup Flow", + description="The name of the flow to run after the scratch org is created.", + examples=["dev_org", "qa_org_2gp"], + ) + noancestors: bool = Field( + None, + title="No Ancestors", + description="If True, the scratch org should be created without ancestors.", + examples=[True, False], + ) + release: Literal["preview", "previous"] = Field( + None, + title="Release", + description="The Salesforce release to use for the scratch org.", + examples=["preview", "previous"], + ) + + class Config: + title = "Scratch Org Configuration" + description = "The scratch org configuration for the project. See https://cumulusci.readthedocs.io/en/stable/config.html#scratch-org-configurations for more information." class Orgs(CCIDictModel): - scratch: Dict[str, ScratchOrg] = None + scratch: Dict[str, ScratchOrg] = Field( + None, + title="Scratch Org Configs", + description="A dictionary of scratch org configurations for the project.", + examples=[ + { + "dev": { + "config_file": "orgs/dev.json", + "days": 1, + "namespaced": True, + "setup_flow": "dev_org", + "noancestors": True, + "release": "preview", + }, + "qa": { + "config_file": "orgs/qa.json", + "days": 7, + "namespaced": True, + "setup_flow": "qa_org", + "noancestors": True, + "release": "previous", + }, + } + ], + ) + + class Config: + title = "Orgs Configuration" + description = "The orgs configuration for the project." class ServiceAttribute(CCIDictModel): - description: str = None - required: bool = None - default_factory: PythonClassPath = None - default: str = None - sensitive: bool = False + description: str = Field( + None, + title="Description", + description="A description of the attribute.", + examples=["The username for the service."], + ) + required: bool = Field( + None, + title="Required", + description="If True, the attribute is required.", + examples=[True, False], + ) + default_factory: PythonClassPath = Field( + None, + title="Default Factory", + description="The Python class path to a callable that returns the default value for the attribute.", + examples=["cumulusci.core.config.OrgConfig.default_username"], + ) + default: str = Field( + None, + title="Default", + description="The default value for the attribute.", + examples=["my_default_username"], + ) + sensitive: bool = Field( + False, + title="Sensitive", + description="If True, the attribute is sensitive and should not be logged.", + examples=[True, False], + ) + + class Config: + title = "Service Attribute Definition" + description = "An attribute for a service." class Service(CCIDictModel): - description: str = None - class_path: Optional[str] - attributes: Dict[str, ServiceAttribute] = None - validator: PythonClassPath = None + description: str = Field( + None, + title="Description", + description="A description of the service.", + examples=["Create a connection to GitHub to interact with the API."], + ) + class_path: Optional[str] = Field( + None, + title="Class Path", + description="The Python class path to the service class. Typically not used for most services that just need simple key/value configuration.", + examples=[ + "cumulusci.core.config.marketing_cloud_service_config.MarketingCloudServiceConfig" + ], + ) + attributes: Dict[str, ServiceAttribute] = Field( + None, + title="Attributes", + description="A dictionary of attributes for the service.", + examples=[ + { + "username": { + "description": "The username for the service.", + "required": True, + "default_factory": "cumulusci.core.config.OrgConfig.default_username", + }, + "password": { + "description": "The password for the service.", + "required": True, + "sensitive": True, + }, + }, + { + "base_url": { + "description": "The base URL for the service.", + "required": True, + "default": "https://my-service.com", + }, + "api_token": { + "description": "The API token for the service.", + "required": True, + "sensitive": True, + }, + }, + ], + ) + validator: PythonClassPath = Field( + None, + title="Validator", + description="The Python class path to a callable that validates the service configuration.", + examples=["cumulusci.core.github.validate_service"], + ) + + class Config: + title = "Service Definition" + description = "A service for the project." class CumulusCIConfig(CCIDictModel): - keychain: PythonClassPath + keychain: PythonClassPath = Field( + ..., + title="Keychain Class Path", + description="The Python class path to the keychain class to override CumulusCI's keychain.", + examples=[ + "cumulusci.core.keychain.EnvironmentProjectKeychain", + "mypypackage.keychain.MyCumulusCIKeychain", + ], + ) + + class Config: + title = "CumulusCI Configuration" + description = "The CumulusCI configuration for the project." class GitHubSourceRelease(StrEnum): @@ -183,15 +890,64 @@ class GitHubSourceRelease(StrEnum): class GitHubSourceModel(HashableBaseModel): - github: str - resolution_strategy: Optional[str] - commit: Optional[str] - ref: Optional[str] - branch: Optional[str] - tag: Optional[str] - release: Optional[GitHubSourceRelease] - description: Optional[str] - allow_remote_code: Optional[bool] = False + github: str = Field( + ..., + title="GitHub URL", + description="The GitHub URL for the repository.", + examples=["https://github.com/SalesforceFoundation/NPSP"], + ) + resolution_strategy: Optional[str] = Field( + None, + title="Resolution Strategy", + description="The resolution strategy to use for the GitHub source.", + examples=["latest_release", "include_beta", "commit_status", "unlocked"], + ) + commit: Optional[str] = Field( + None, + title="Commit", + description="The commit to use for the GitHub source.", + examples=["abc123"], + ) + ref: Optional[str] = Field( + None, + title="Ref", + description="The ref to use for the GitHub source.", + examples=["main"], + ) + branch: Optional[str] = Field( + None, + title="Branch", + description="The branch to use for the GitHub source.", + examples=["main"], + ) + tag: Optional[str] = Field( + None, + title="Tag", + description="The tag to use for the GitHub source.", + examples=["v1.0"], + ) + release: Optional[GitHubSourceRelease] = Field( + None, + title="Release", + description="The release to use for the GitHub source.", + examples=["latest", "previous", "latest_beta"], + ) + description: Optional[str] = Field( + None, + title="Description", + description="A description of the source.", + examples=["The GitHub source for the Nonprofit Success Pack."], + ) + allow_remote_code: Optional[bool] = Field( + False, + title="Allow Remote Code", + description="If True, allow remote code execution for the source. This is a security risk and should only be used for trusted sources.", + examples=[True, False], + ) + + class Config: + title = "GitHub Source Definition" + description = "A GitHub source for the project. See https://cumulusci.readthedocs.io/en/stable/config.html#tasks-and-flows-from-a-different-project for more information." @root_validator def validate(cls, values): @@ -215,26 +971,356 @@ def validate(cls, values): class LocalFolderSourceModel(HashableBaseModel): - path: DirectoryPath - allow_remote_code: Optional[bool] = False + path: DirectoryPath = Field( + ..., + title="Folder Path", + description="The path to the folder.", + examples=["modules"], + ) + allow_remote_code: Optional[bool] = Field( + False, + title="Allow Remote Code", + description="If True, allow remote code execution for the source. This is a security risk and should only be used for trusted sources.", + examples=[True, False], + ) + + class Config: + title = "Local Folder Source Definition" + description = "A local folder source for the project. See https://cumulusci.readthedocs.io/en/stable/config.html#tasks-and-flows-from-a-different-project for more information." class CumulusCLIConfig(CCIDictModel): - show_stacktraces: bool = False - plain_output: bool = None + show_stacktraces: bool = Field( + False, + title="Show Stacktraces", + description="If True, show stacktraces for exceptions.", + examples=[True, False], + ) + plain_output: bool = Field( + None, + title="Plain Output", + description="If True, use plain output instead of rich output.", + examples=[True, False], + ) + + class Config: + title = "CumulusCI CLI Configuration" + description = "The CumulusCI CLI configuration for the project." class CumulusCIRoot(CCIDictModel): - tasks: Dict[str, Task] = {} - flows: Dict[str, Flow] = {} - project: Project = {} - orgs: Orgs = {} - services: Dict[str, Service] = {} - cumulusci: CumulusCIConfig = None - plans: Dict[str, Plan] = {} - minimum_cumulusci_version: str = None - sources: Dict[str, Union[LocalFolderSourceModel, GitHubSourceModel]] = {} - cli: CumulusCLIConfig = None + tasks: Dict[str, Task] = Field( + {}, + title="Tasks", + description="A dictionary of tasks for the project.", + examples=[ + { + "deploy": { + "class_path": "cumulusci.tasks.salesforce.Deploy", + "description": "Deploys the metadata in the project to the org.", + "group": "Deployment", + "options": {"path": "src"}, + }, + "load_custom_settings": { + "class_path": "cumulusci.tasks.salesforce.LoadCustomSettings", + "description": "Loads custom settings data into the org.", + "group": "Data", + "options": {"path": "data/custom_settings"}, + }, + } + ], + ) + flows: Dict[str, Flow] = Field( + {}, + title="Flows", + description="A dictionary of flows for the project.", + examples=[ + { + "dependencies": { + "description": "Installs all dependencies for the project.", + "steps": { + "1": {"task": "update_dependencies"}, + "2": {"task": "install_prod"}, + }, + }, + "config_dev": { + "description": "Configures the org for development.", + "steps": { + "1": {"flow": "config_common"}, + "2": {"flow": "config_dev"}, + }, + }, + "dev_org": { + "description": "Sets up a new dev org.", + "steps": { + "1": {"flow": "dependencies"}, + "2": {"flow": "deploy_unmanaged"}, + "3": {"flow": "config_dev"}, + }, + }, + } + ], + ) + project: Project = Field( + {}, + title="Project", + description="The project configuration.", + examples=[ + { + "name": "Nonprofit Success Pack", + "package": { + "name": "Nonprofit Success Pack", + "namespace": "npsp", + "api_version": "59.0", + }, + "test": {"name_match": "%Test"}, + "git": { + "repo_url": "https://github.com/SalesforceFoundation/NPSP", + "default_branch": "main", + "prefix_feature": "feature/", + "prefix_beta": "beta/", + "prefix_release": "release/", + "push_prefix_sandbox": "sandbox/", + "push_prefix_production": "prod/", + }, + "dependencies": [ + {"namespace": "somens", "version": "1.23"}, + {"github": "https://github.com/SalesforceFoundation/Households"}, + ], + "dependency_resolutions": { + "production": "latest_release", + "preproduction": "include_beta", + "resolution_strategies": { + "latest_release": ["tag", "latest_release", "unmanaged"], + "include_beta": [ + "tag", + "latest_beta", + "latest_release", + "unmanaged", + ], + }, + }, + } + ], + ) + orgs: Orgs = Field( + {}, + title="Orgs", + description="A dictionary of org configurations for the project.", + examples=[ + { + "scratch": { + "dev": { + "config_file": "orgs/dev.json", + "days": 1, + "namespaced": True, + "setup_flow": "dev_org", + "noancestors": True, + "release": "preview", + }, + "qa": { + "config_file": "orgs/qa.json", + "days": 7, + "namespaced": True, + "setup_flow": "qa_org", + "noancestors": True, + "release": "previous", + }, + } + } + ], + ) + services: Dict[str, Service] = Field( + {}, + title="Services", + description="A dictionary of services for the project.", + examples=[ + { + "github": { + "description": "Create a connection to GitHub to interact with the API.", + "attributes": { + "username": { + "description": "The username for the service.", + "required": True, + "default_factory": "cumulusci.core.config.OrgConfig.default_username", + }, + "password": { + "description": "The password for the service.", + "required": True, + "sensitive": True, + }, + }, + "validator": "cumulusci.core.github.validate_service", + } + } + ], + ) + cumulusci: CumulusCIConfig = Field( + None, + title="CumulusCI", + description="The CumulusCI configuration.", + examples=[ + { + "keychain": "cumulusci.core.keychain.EnvironmentProjectKeychain", + } + ], + ) + plans: Dict[str, Plan] = Field( + {}, + title="Plans", + description="A dictionary of plans for the project.", + examples=[ + { + "base_install": { + "title": "Base Install", + "description": "A base install of the packages only", + "tier": "primary", + "slug": "base-install", + "is_listed": True, + "steps": { + "1": {"task": "update_dependencies"}, + "2": {"task": "install_prod"}, + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org.", + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org.", + }, + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"], + }, + "full_demo_install": { + "title": "Full Demo Install", + "description": "A full demo install of the packages with configuration and sample data", + "tier": "secondary", + "slug": "full-demo-install", + "is_listed": True, + "steps": { + "1": {"task": "update_dependencies"}, + "2": {"task": "install_prod"}, + "3": {"flow": "config_common"}, + "4": {"flow": "config_demo"}, + "5": {"flow": "demo_data"}, + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org.", + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org.", + }, + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"], + }, + "customer_org": { + "title": "Customer Org", + "description": "A customer org with the packages installed", + "tier": "additional", + "slug": "customer-org", + "is_listed": True, + "steps": { + "1": {"flow": "customer_org"}, + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org.", + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org.", + }, + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"], + }, + "customer_org_full": { + "title": "Customer Org Full", + "description": "A customer org with the packages installed and configured", + "tier": "additional", + "slug": "customer-org-full", + "is_listed": True, + "steps": { + "1": {"task": "customer_org_full"}, + }, + "checks": [ + { + "when": "org.scratch == True", + "action": "error", + "message": "The org must be a scratch org.", + }, + { + "when": "'npsp' not in tasks.get_installed_packages()", + "action": "error", + "message": "NPSP must be installed in your org.", + }, + ], + "error_message": "The org must be a scratch org.", + "post_install_message": "The plan has been installed.", + "preflight_message": "The plan is about to be installed. We're running some checks on your org first.", + "allowed_org_providers": ["devhub", "user"], + }, + } + ], + ) + minimum_cumulusci_version: str = Field( + None, + title="Minimum CumulusCI Version", + description="The minimum version of CumulusCI required to run the project.", + examples=["3.0.0"], + ) + sources: Dict[str, Union[LocalFolderSourceModel, GitHubSourceModel]] = Field( + {}, + title="Sources", + description="A dictionary of sources for the project.", + examples=[ + { + "npsp": { + "github": "https://github.com/SalesforceFoundation/NPSP", + "resolution_strategy": "latest_release", + }, + "pmm": { + "github": "https://github.com/SalesforceFoundation/PMM", + "resolution_strategy": "latest_release", + }, + }, + ], + ) + cli: CumulusCLIConfig = Field( + None, + title="CumulusCI CLI", + description="The CumulusCI CLI configuration.", + examples=[ + { + "show_stacktraces": False, + "plain_output": False, + } + ], + ) + + class Config: + title = "CumulusCI Project Configuration" + description = "The root configuration for the project." @validator("plans") def validate_plan_tiers(cls, plans): From e572ccd821825d18e6d800f4cd26bd99f390f775 Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Tue, 19 Dec 2023 13:25:44 -0600 Subject: [PATCH 2/3] Fixed test failure due to integer vs string keys for step numbers --- cumulusci/utils/yaml/cumulusci_yml.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index f0bd6256bc..a1c5370a18 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -323,27 +323,27 @@ class ReleaseNotes(CCIDictModel): description="A dictionary of release notes parsers in execution order.", examples=[ { - 1: { + "1": { "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", "title": "Critical Changes", }, - 2: { + "2": { "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", "title": "Changes", }, - 3: { + "3": { "class_path": "cumulusci.tasks.release_notes.parser.GithubIssuesParser", "title": "Issues Closed", }, - 4: { + "4": { "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", "title": "New Metadata", }, - 5: { + "5": { "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", "title": "Deleted Metadata", }, - 6: { + "6": { "class_path": "cumulusci.tasks.release_notes.parser.InstallLinkParser", "title": "Installation Info", }, @@ -406,11 +406,11 @@ class Git(CCIDictModel): examples=[ { "parsers": { - 1: { + "1": { "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", "title": "Critical Changes", }, - 2: { + "2": { "class_path": "cumulusci.tasks.release_notes.parser.GithubLinesParser", "title": "Changes", }, From aba386bc23cd32f9c5b0eb5f9131bffda2da6df0 Mon Sep 17 00:00:00 2001 From: Jason Lantz Date: Wed, 20 Dec 2023 10:53:03 -0600 Subject: [PATCH 3/3] Update StepUIOptions model based on MetaDeploy code --- cumulusci/utils/yaml/cumulusci_yml.py | 36 ++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/cumulusci/utils/yaml/cumulusci_yml.py b/cumulusci/utils/yaml/cumulusci_yml.py index a1c5370a18..7595318e56 100644 --- a/cumulusci/utils/yaml/cumulusci_yml.py +++ b/cumulusci/utils/yaml/cumulusci_yml.py @@ -69,11 +69,24 @@ class Config: description = "A preflight check to run before a step or plan in MetaDeploy." +class StepUIOptionsKindEnum(StrEnum): + metadata = "Metadata" + onetime = "One Time Apex" + managed = "Managed Package" + data = "Data" + other = "Other" + + class StepUIOptions(TypedDict): - name: str # Name of the step shown to the user. - is_required: bool # If True, the step will always be run. - is_recommended: bool # If True, the step will be run unless the user opts out. - is_optional: bool # If True, the step will be run only if the user opts in. + name: Optional[str] # Name of the step shown to the user. + description: Optional[str] # Description of the step shown to the user. + is_required: Optional[bool] # If True, the step will always be run. + is_recommended: Optional[ + bool + ] # If True, the step will be run unless the user opts out. + kind: Optional[ + StepUIOptionsKindEnum + ] # The kind of step. Used to group steps in the UI. class Step(CCIDictModel): @@ -117,8 +130,19 @@ class Step(CCIDictModel): title="UI Options", description="Options for the task or flow that are only used by MetaDeploy. For flows, this should be a dictionary with a key for the task name containing keys for each option.", examples=[ - {"name": "Deploy Recommended Config", "is_recommended": True}, - {"name": "Deploy Optional Config", "is_optional": True}, + {"name": "Deploy Required Config", "kind": "managed"}, + { + "name": "Deploy Recommended Config", + "kind": "metadata", + "is_required": False, + "is_recommended": True, + }, + { + "name": "Deploy Optional Config", + "kind": "other", + "is_required": False, + "is_recommended": False, + }, ], ) checks: List[PreflightCheck] = Field(