Skip to content

DEVOPS-688 feat: add support for clariti org pooling#5

Merged
dipakparmar merged 14 commits intomainfrom
feature/DEVOPS-688
Oct 10, 2025
Merged

DEVOPS-688 feat: add support for clariti org pooling#5
dipakparmar merged 14 commits intomainfrom
feature/DEVOPS-688

Conversation

@dipakparmar
Copy link
Copy Markdown

@dipakparmar dipakparmar commented Oct 10, 2025

Summary by CodeRabbit

  • New Features

    • Import orgs from Clariti pools with a new --pool-id option; falls back to existing CLI flow when omitted. Optional username/alias and org name supported; default org naming and CLI alias synchronization added. Improved Clariti error reporting.
  • Documentation

    • Added a comprehensive contributor/project guide and updated user docs with Clariti pool import workflow and revised import command examples.
  • Tests

    • New unit tests covering CLI validation and Clariti integration behaviors.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 10, 2025

Walkthrough

Adds Clariti support: a new clariti utility module, extends the cci org import CLI to accept --pool-id (with mutual exclusion vs username), synchronizes SF aliases, preserves legacy keychain import, adds unit tests, updates docs, and adds repository guidelines in AGENTS.md.

Changes

Cohort / File(s) Summary
Docs
AGENTS.md, docs/headless.md, docs/scratch-orgs.md
New repository guidelines and updates documenting Clariti pool-based import, revised command examples and alias/org naming guidance, and error/usage notes.
CLI: org import flow
cumulusci/cli/org.py
org_import made args optional, adds --pool-id option, resolves/uses Clariti pool when requested, checks out orgs, derives default org name, syncs SF alias, converts Clariti failures to Click errors, and falls back to legacy keychain import.
Clariti utilities
cumulusci/utils/clariti.py
New module introducing ClaritiError, ClaritiCheckoutResult, resolve_pool_id, checkout_org_from_pool, set_sf_alias, _extract_string, _summarize_error_output, and build_default_org_name; wraps sf clariti calls and parses results/errors.
Tests
cumulusci/tests/test_cli_org_import.py, cumulusci/tests/test_utils_clariti.py
New tests covering CLI validation (mutual exclusion of username and --pool-id), Clariti pool resolution, checkout parsing across payload shapes, alias setting, error handling (including debug), and default org name logic.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant CLI as "cci org import"
    participant Clariti as "cumulusci.utils.clariti"
    participant SF as "sf CLI"
    participant Keychain as "CCI Keychain"

    rect rgba(220,235,255,0.45)
    note over CLI,Clariti: Clariti-based checkout path (new)
    User->>CLI: cci org import [USERNAME?] [--org <name>] [--pool-id <id>]
    alt pool-id provided OR username omitted
        CLI->>Clariti: resolve_pool_id(pool_id, project_root)
        Clariti-->>CLI: pool_id or error
        CLI->>Clariti: checkout_org_from_pool(pool_id, alias?)
        Clariti->>SF: sf clariti org checkout --json
        SF-->>Clariti: stdout (JSON) / exit status
        Clariti-->>CLI: ClaritiCheckoutResult(username, alias, org_id, pool_id)
        opt alias sync
            CLI->>Clariti: set_sf_alias(alias, username)
            Clariti->>SF: sf alias set ...
            SF-->>Clariti: status
            Clariti-->>CLI: success/failure
        end
        CLI->>Keychain: import org using username (+derived org_name)
        Keychain-->>CLI: persisted
        CLI-->>User: Imported org (Clariti checkout)
    else
        note over CLI,Keychain: Legacy keychain import (unchanged)
        User->>CLI: cci org import <username_or_alias> --org <name>
        CLI->>Keychain: import from SFDX keychain
        Keychain-->>CLI: persisted
        CLI-->>User: Imported org (legacy)
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

Thump-thump, I hop to checkout day,
Pools and aliases show the way,
JSON crumbs and CLI lights,
I stitch names neat through coding nights,
A carrot hop — import hooray! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The pull request includes AGENTS.md, a new document containing repository guidelines that are unrelated to the feature of Clariti org pooling. This file introduces content on project structure, coding style, and general development workflows rather than org pooling. None of the objectives of DEVOPS-688 call for broad documentation of repository standards. Therefore, AGENTS.md falls outside the scope of the linked issue’s objectives. AGENTS.md should be removed from this PR or migrated to a separate PR dedicated to repository-wide documentation. Keeping unrelated documentation in this feature branch may obscure review focus and inflate the changeset. Isolating this file in its own PR will improve maintainability and traceability.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely summarizes the primary change of adding Clariti org pooling support. It clearly references the main feature introduced without including noise or file lists. A teammate scanning history will understand the change from the title alone.
Linked Issues Check ✅ Passed The PR adds the optional --pool-id option and updates org_import to read from .clariti.json when not provided. It raises clear errors for missing pool IDs and failed checkouts through the ClaritiError mechanism. The integration uses the sf clariti plugin to handle persistent org marking and alias management. Unit tests cover CLI validation, utility functions, and error scenarios. These changes fulfill all coding-related requirements of DEVOPS-688.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/DEVOPS-688

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

@dipakparmar dipakparmar marked this pull request as ready for review October 10, 2025 18:53
@dipakparmar dipakparmar marked this pull request as draft October 10, 2025 18:54
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
cumulusci/utils/clariti.py (4)

107-116: Add a timeout and handle CLI hangs in Clariti checkout.

External CLI calls can hang indefinitely. Add a reasonable timeout and surface a clear error on TimeoutExpired.

-def checkout_org_from_pool(
-    pool_id: str,
-    *,
-    alias: Optional[str] = None,
-    env: Optional[Dict[str, str]] = None,
-) -> ClaritiCheckoutResult:
+def checkout_org_from_pool(
+    pool_id: str,
+    *,
+    alias: Optional[str] = None,
+    env: Optional[Dict[str, str]] = None,
+    timeout: float = 300.0,
+) -> ClaritiCheckoutResult:
@@
-    try:
-        proc = subprocess.run(
+    try:
+        proc = subprocess.run(
             command,
             check=False,
             text=True,
             capture_output=True,
-            env=env,
+            env=env,
+            timeout=timeout,
         )
-    except FileNotFoundError as err:
+    except subprocess.TimeoutExpired as err:
+        raise ClaritiError(
+            f"Timed out waiting for Clariti checkout after {timeout:.0f}s."
+        ) from err
+    except FileNotFoundError as err:
         raise ClaritiError("Salesforce CLI 'sf' was not found on PATH.") from err
@@
-    try:
-        payload = json.loads(stdout)
+    try:
+        payload = json.loads(stdout)
     except json.JSONDecodeError as err:
-        raise ClaritiError(
-            "Failed to parse JSON from Clariti CLI response."
-            f" Raw output: {stdout}"
-        ) from err
+        # Avoid dumping unbounded output; include a short snippet for diagnostics.
+        snippet = stdout if len(stdout) <= 2000 else stdout[:2000] + " ... [truncated]"
+        raise ClaritiError(
+            "Failed to parse JSON from Clariti CLI response. "
+            f"Raw output (snippet): {snippet}"
+        ) from err

Also applies to: 118-136


139-151: Broaden JSON extraction to handle nested result shapes.

Clariti/SFCLIs often nest data under result. Add fallbacks similar to username/alias.

-    org_id_value = _extract_string(
-        payload, (("orgId",),), allow_missing=True
-    )
-    instance_url_value = _extract_string(
-        payload, (("instanceUrl",),), allow_missing=True
-    )
-    org_type_value = _extract_string(
-        payload, (("orgType",),), allow_missing=True
-    )
-    pool_id_value = _extract_string(
-        payload, (("poolId",),), allow_missing=True
-    )
+    org_id_value = _extract_string(
+        payload, (("orgId",), ("result", "orgId")), allow_missing=True
+    )
+    instance_url_value = _extract_string(
+        payload, (("instanceUrl",), ("result", "instanceUrl")), allow_missing=True
+    )
+    org_type_value = _extract_string(
+        payload, (("orgType",), ("result", "orgType")), allow_missing=True
+    )
+    pool_id_value = _extract_string(
+        payload, (("poolId",), ("result", "poolId")), allow_missing=True
+    )

213-217: Improve error context from _extract_string.

When a required field is missing, include searched paths to aid debugging.

-    raise ClaritiError("Unable to determine required field from Clariti response.")
+    searched = [".".join(p) for p in paths]
+    raise ClaritiError(
+        "Unable to determine required field from Clariti response. "
+        f"Searched paths: {', '.join(searched)}"
+    )

171-181: Also add a timeout to alias setting.

Same external-call risk applies to sf alias set. Add a timeout and handle it.

-def set_sf_alias(
-    alias: str, username: str, *, env: Optional[Dict[str, str]] = None
-) -> Tuple[bool, Optional[str]]:
+def set_sf_alias(
+    alias: str,
+    username: str,
+    *,
+    env: Optional[Dict[str, str]] = None,
+    timeout: float = 30.0,
+) -> Tuple[bool, Optional[str]]:
@@
-    try:
-        proc = subprocess.run(
+    try:
+        proc = subprocess.run(
             command,
             check=False,
             text=True,
             capture_output=True,
-            env=env,
+            env=env,
+            timeout=timeout,
         )
+    except subprocess.TimeoutExpired:
+        return False, f"Timed out setting alias after {timeout:.0f}s."
     except FileNotFoundError:
         return False, "Salesforce CLI 'sf' was not found on PATH."

Also applies to: 183-191

cumulusci/cli/org.py (2)

312-321: Guard against accidental overwrite of an existing org name.

SfdxOrgConfig(...).save() will overwrite an existing entry. Align with other commands by invoking runtime.check_org_overwrite(org_name) first.

     if not username_or_alias:
         raise click.UsageError(
             "Please provide a username or alias, or specify a Clariti pool id."
         )
     if not org_name:
         raise click.UsageError("Please specify ORGNAME or --org ORGNAME.")
+    # Avoid unintentionally overwriting an existing org entry
+    runtime.check_org_overwrite(org_name)

291-311: Alias alignment UX looks good; consider surfacing a hint when Clariti already set a different alias.

Non-blocking: You print the Clariti-assigned alias and then set the CCI/sf alias to org_name. That creates two aliases for the same username, which is fine. Optionally, suggest how to remove the old alias (sf alias unset <name>) if users want a single alias.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6e609ac and ff3eb68.

📒 Files selected for processing (3)
  • AGENTS.md (1 hunks)
  • cumulusci/cli/org.py (2 hunks)
  • cumulusci/utils/clariti.py (1 hunks)

Comment thread cumulusci/cli/org.py
…nd --pool-id conflict; enhance error handling in checkout_org_from_pool and set_sf_alias
Comment thread cumulusci/cli/org.py
…ptional pool ID; enhance docstrings for clarity
@dipakparmar dipakparmar marked this pull request as ready for review October 10, 2025 20:31
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cumulusci/cli/org.py (1)

258-263: Reflect Click’s optional arguments in type hints.

username_or_alias and org_name can be None when omitted on the CLI, so the signature should use Optional[str] to match actual values and keep type checkers honest.

Apply this diff:

 def org_import(
     runtime: CliRuntime,
-    username_or_alias: str,
-    org_name: str,
+    username_or_alias: Optional[str],
+    org_name: Optional[str],
     pool_id: Optional[str] = None,
 ):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ff3eb68 and 63cd07b.

📒 Files selected for processing (2)
  • cumulusci/cli/org.py (2 hunks)
  • cumulusci/utils/clariti.py (1 hunks)

Comment thread cumulusci/utils/clariti.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cumulusci/tests/test_cli_org_import.py (1)

9-16: Clarify the purpose of _load_keychain or remove it.

The _load_keychain method is defined but never called in the test. If it's intended to override a method from a parent class that would be called during test execution, consider adding a docstring explaining this. Otherwise, remove it to reduce confusion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 63cd07b and 0316982.

📒 Files selected for processing (4)
  • cumulusci/tests/test_cli_org_import.py (1 hunks)
  • cumulusci/tests/test_utils_clariti.py (1 hunks)
  • docs/headless.md (1 hunks)
  • docs/scratch-orgs.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • docs/scratch-orgs.md
🔇 Additional comments (15)
docs/headless.md (1)

125-126: LGTM! Documentation reflects the updated CLI syntax.

The updated command syntax clearly specifies the alias-based import approach and provides helpful clarification about the second positional argument.

cumulusci/tests/test_cli_org_import.py (1)

18-32: LGTM! Test correctly validates mutual exclusivity.

The test properly validates that the CLI rejects conflicting inputs (both username/alias and pool_id) and checks for appropriate error messaging.

cumulusci/tests/test_utils_clariti.py (13)

16-17: LGTM! Test validates explicit pool_id precedence.

The test correctly verifies that an explicitly provided pool_id takes precedence over config-based resolution.


20-26: LGTM! Test validates behavior with missing config key.

The test properly validates that when the config file exists but doesn't contain the required pool_id key, None is returned.


29-35: LGTM! Test validates missing config file error.

The test correctly verifies that a ClaritiError is raised when the config file doesn't exist and checks for appropriate error messaging.


38-60: LGTM! Test validates successful org checkout with username parsing.

The test properly validates the parsing of the checkout response payload and ensures all fields are correctly extracted into the ClaritiCheckoutResult.


63-81: LGTM! Test validates nested username extraction.

The test correctly verifies that usernames can be extracted from nested result structures, which handles different Clariti CLI response formats.


83-98: LGTM! Test validates pool_id omission behavior.

The test properly verifies that when pool_id is None, the CLI command is invoked without the --pool-id flag, allowing the Clariti CLI to use its default pool configuration.


100-112: LGTM! Test validates plain text error handling.

The test correctly verifies that ClaritiError is raised with appropriate error messaging when the Clariti CLI returns a non-zero exit code with plain text stderr.


114-134: LGTM! Test validates JSON-formatted error handling.

The test properly verifies that structured JSON errors from the Clariti CLI are parsed and formatted into user-friendly error messages.


136-159: LGTM! Test validates debug mode error formatting.

The test correctly verifies that in debug mode, the full raw JSON response is included in the error message for troubleshooting purposes.


161-172: LGTM! Test validates successful alias setting.

The test properly verifies that the sf alias set command is correctly constructed and executed, and that success is properly returned.


174-184: LGTM! Test validates alias setting failure.

The test correctly verifies that alias setting failures are properly detected and error messages are returned.


187-188: LGTM! Test validates alias preference in default org name.

The test properly verifies that when an alias is provided, it's used as the default org name rather than the username.


191-195: LGTM! Test validates username sanitization.

The test correctly verifies that special characters in usernames are sanitized when building the default org name, ensuring compatibility with CumulusCI's org naming requirements.

Comment thread cumulusci/tests/test_cli_org_import.py Outdated
Comment thread cumulusci/cli/org.py
Comment thread cumulusci/cli/org.py
dipakparmar and others added 2 commits October 10, 2025 15:05
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Dipak Parmar <hi@dipak.tech>
…") decorator.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Dipak Parmar <hi@dipak.tech>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
cumulusci/utils/clariti.py (1)

179-214: Consider clarifying the error message prefix.

The function correctly sets the SF CLI alias with proper error handling. However, the error message "Clariti alias failed" (line 209) might be slightly misleading since sf alias set is a standard Salesforce CLI command, not Clariti-specific.

Consider this change for clarity:

-        summary = f"Clariti alias failed: {summary}"
+        summary = f"Failed to set SF alias: {summary}"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0316982 and 479a9c2.

📒 Files selected for processing (1)
  • cumulusci/utils/clariti.py (1 hunks)
🔇 Additional comments (8)
cumulusci/utils/clariti.py (8)

1-13: LGTM! Well-structured module with appropriate imports.

The module docstring clearly describes the purpose, and all necessary imports are present. The type hints and error handling dependencies are properly imported.


15-38: LGTM! Clean exception and dataclass definitions.

The ClaritiError exception provides a clear, specific type for Clariti-related failures. The ClaritiCheckoutResult dataclass is well-documented with appropriate field types, clearly distinguishing required (username) from optional metadata fields.


41-70: LGTM! Solid pool ID resolution logic.

The function correctly handles both explicit pool IDs and implicit configuration via .clariti.json. The error messages are clear and actionable, guiding users to provide --pool-id or ensure the config file exists. Returning None when the config file is present correctly delegates to the Clariti CLI.


73-88: LGTM! Robust multi-path extraction strategy.

Defining multiple search paths for username and alias fields provides excellent resilience against varying Clariti CLI response structures. This approach ensures the utility can adapt to different response formats without breaking.


91-176: LGTM! Comprehensive checkout implementation with excellent error handling.

The function demonstrates strong practices:

  • Secure subprocess execution (list arguments, no shell=True)
  • Comprehensive error handling for missing CLI, subprocess failures, and JSON parsing
  • Debug mode integration for detailed diagnostics
  • Required vs. optional field extraction properly differentiated
  • Clear error messages with context

217-246: LGTM! Robust field extraction with clear semantics.

The helper correctly traverses nested dictionaries, validates types, and handles missing values appropriately. The allow_missing parameter provides clear control over required vs. optional fields.


249-265: LGTM! Alias sanitization concern from previous review has been addressed.

The function now correctly sanitizes the Clariti-provided alias (lines 258-261) using the same pattern as the username fallback, preventing issues with special characters in sf alias set commands and keychain filenames. The implementation matches the previous review's suggested fix exactly.

Based on learnings.


268-301: LGTM! Comprehensive error message extraction.

The helper effectively handles multiple error response formats by attempting JSON parsing and extracting common error fields (message, error, msg, code, name, status). The fallback behavior ensures users receive meaningful feedback regardless of response structure. Lines 299-301 correctly return the full JSON as both summary and raw output when no more concise message can be extracted.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 53d3ee0 and 26a1f81.

📒 Files selected for processing (1)
  • cumulusci/utils/clariti.py (1 hunks)

Comment thread cumulusci/utils/clariti.py
@dipakparmar dipakparmar merged commit f574073 into main Oct 10, 2025
1 check was pending
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
cumulusci/utils/clariti.py (1)

153-166: Enhance error specificity for missing required fields.

When _extract_string fails to locate a required field, it raises a generic "Unable to determine required field from Clariti response." Without knowing which field (username, org_id, etc.) is missing, troubleshooting becomes harder. Consider passing a field name parameter to _extract_string so the error can report the specific missing field.

Apply this diff to enhance error messages:

 def _extract_string(
     payload: Dict[str, Any],
     paths: Sequence[Sequence[str]],
     *,
     allow_missing: bool = False,
+    field_name: Optional[str] = None,
 ) -> Optional[str]:
     """Extract the first non-empty string value from the payload.
 
     :param payload: Data structure returned from Clariti.
     :param paths: Candidate key paths to inspect.
     :param allow_missing: Whether to return ``None`` when no value is found.
+    :param field_name: Name of the field for error reporting.
     :returns: The first matching string or ``None``.
     :raises ClaritiError: if no value is found and ``allow_missing`` is false.
     """
 
     for path in paths:
         value: Any = payload
         for key in path:
             if isinstance(value, dict) and key in value:
                 value = value[key]
             else:
                 value = None
                 break
         if isinstance(value, str) and value.strip():
             return value.strip()
 
     if allow_missing:
         return None
 
-    raise ClaritiError("Unable to determine required field from Clariti response.")
+    error_msg = "Unable to determine required field from Clariti response."
+    if field_name:
+        error_msg = f"Unable to determine '{field_name}' from Clariti response."
+    raise ClaritiError(error_msg)

Then update the call sites:

-    username = cast(str, _extract_string(payload, _USERNAME_PATHS))
+    username = cast(str, _extract_string(payload, _USERNAME_PATHS, field_name="username"))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 26a1f81 and c588e7e.

📒 Files selected for processing (1)
  • cumulusci/utils/clariti.py (1 hunks)

@dipakparmar dipakparmar deleted the feature/DEVOPS-688 branch October 12, 2025 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants